recappi 0.1.45 → 0.1.47

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,10 +59,19 @@ 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
63
  if (event.input === "microphone") {
63
- return { ...telemetry, level: { ...telemetry.level, mic: level } };
64
+ return {
65
+ ...telemetry,
66
+ level: { ...telemetry.level, mic: level },
67
+ levelDb: { ...telemetry.levelDb, ...levelDb3 != null ? { mic: levelDb3 } : {} }
68
+ };
64
69
  }
65
- return { ...telemetry, level: { ...telemetry.level, system: level } };
70
+ return {
71
+ ...telemetry,
72
+ level: { ...telemetry.level, system: level },
73
+ levelDb: { ...telemetry.levelDb, ...levelDb3 != null ? { system: levelDb3 } : {} }
74
+ };
66
75
  }
67
76
  if (event.type === "error") {
68
77
  if (isLiveCaptionSidecarError(event.code)) return telemetry;
@@ -152,7 +161,7 @@ function statusStyle(status) {
152
161
  case "failed":
153
162
  return { label: "Failed", color: "red" };
154
163
  default:
155
- return { label: status, color: "white" };
164
+ return { label: status, color: "gray" };
156
165
  }
157
166
  }
158
167
  function spinnerChar(frame) {
@@ -202,10 +211,6 @@ function jobDetail(item) {
202
211
  if (item.status === "failed") return "failed";
203
212
  return "";
204
213
  }
205
- function padCell(text, width) {
206
- if (text.length > width) return `${text.slice(0, Math.max(0, width - 1))}\u2026`;
207
- return text.padEnd(width);
208
- }
209
214
  function charWidth(code) {
210
215
  if (code >= 4352 && code <= 4447 || // Hangul Jamo
211
216
  code === 9001 || code === 9002 || code >= 11904 && code <= 12350 || // CJK radicals … Kangxi
@@ -228,23 +233,6 @@ function displayWidth(text) {
228
233
  for (const ch of text) width += charWidth(ch.codePointAt(0) ?? 0);
229
234
  return width;
230
235
  }
231
- function padDisplay(text, width) {
232
- const w = displayWidth(text);
233
- if (w === width) return text;
234
- if (w < width) return text + " ".repeat(width - w);
235
- let out = "";
236
- let acc = 0;
237
- for (const ch of text) {
238
- const cw = charWidth(ch.codePointAt(0) ?? 0);
239
- if (acc + cw > width - 1) break;
240
- out += ch;
241
- acc += cw;
242
- }
243
- out += "\u2026";
244
- acc += 1;
245
- if (acc < width) out += " ".repeat(width - acc);
246
- return out;
247
- }
248
236
  function countJobs(items) {
249
237
  const counts = {
250
238
  total: items.length,
@@ -297,7 +285,7 @@ function recordingStatusStyle(status) {
297
285
  case "aborted":
298
286
  return { label: "Aborted", color: "gray", glyph: "\u2022" };
299
287
  default:
300
- return { label: status, color: "white", glyph: "\u2022" };
288
+ return { label: status, color: "gray", glyph: "\u2022" };
301
289
  }
302
290
  }
303
291
  function listWindow(selected, total, size) {
@@ -565,7 +553,7 @@ var init_LiveCaptionsView = __esm({
565
553
  init_liveCaptions();
566
554
  STATUS_COLOR = {
567
555
  connecting: "yellow",
568
- live: "red",
556
+ live: "cyan",
569
557
  reconnecting: "yellow",
570
558
  stopped: "gray",
571
559
  error: "red"
@@ -617,6 +605,43 @@ function waveform(samples, width) {
617
605
  });
618
606
  return "\u2581".repeat(Math.max(0, pad)) + cells.join("");
619
607
  }
608
+ function levelDb(level) {
609
+ if (level <= 0.03) return "silent";
610
+ return `${Math.round(level * 60 - 60)} dB`;
611
+ }
612
+ function MeterRow({
613
+ label,
614
+ samples,
615
+ level,
616
+ paused,
617
+ width
618
+ }) {
619
+ const silent = level <= 0.03;
620
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
621
+ /* @__PURE__ */ jsx3(Box2, { width: 9, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: label }) }),
622
+ /* @__PURE__ */ jsx3(Box2, { width, children: /* @__PURE__ */ jsx3(Text2, { color: paused ? "gray" : silent ? "yellow" : "cyan", children: waveform(samples, width) }) }),
623
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` ${paused ? "paused" : levelDb(level)}` })
624
+ ] });
625
+ }
626
+ function ProgressBar({ fraction, width = 12 }) {
627
+ const f = Math.max(0, Math.min(1, fraction));
628
+ const filled = Math.round(f * width);
629
+ return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", children: [
630
+ "\u2593".repeat(filled),
631
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2591".repeat(Math.max(0, width - filled)) })
632
+ ] });
633
+ }
634
+ function stoppedPhase(artifact) {
635
+ if (!artifact) return null;
636
+ if (artifact.uploadStatus === "uploading") {
637
+ return { label: "Uploading to Recappi Cloud", fraction: artifact.uploadProgress };
638
+ }
639
+ if (artifact.uploadStatus === "queued") return { label: "Queued to upload" };
640
+ if (artifact.transcriptionStatus === "processing") {
641
+ return { label: "Transcribing", fraction: artifact.transcriptionProgress };
642
+ }
643
+ return null;
644
+ }
620
645
  function RecordingHeroScreen({
621
646
  telemetry,
622
647
  artifact,
@@ -627,11 +652,12 @@ function RecordingHeroScreen({
627
652
  }) {
628
653
  const size = useTerminalSize();
629
654
  const [tick, setTick] = useState3(() => now());
630
- const [wave, setWave] = useState3([]);
655
+ const [waveSys, setWaveSys] = useState3([]);
656
+ const [waveMic, setWaveMic] = useState3([]);
631
657
  useEffect2(() => {
632
658
  if (telemetry.level == null) return;
633
- const lvl = Math.max(telemetry.level.system ?? 0, telemetry.level.mic ?? 0);
634
- setWave((w) => [...w.slice(-512), lvl]);
659
+ setWaveSys((w) => [...w.slice(-256), telemetry.level.system ?? 0]);
660
+ setWaveMic((w) => [...w.slice(-256), telemetry.level.mic ?? 0]);
635
661
  }, [telemetry.level]);
636
662
  useEffect2(() => {
637
663
  const id = setInterval(() => setTick(now()), 1e3);
@@ -641,17 +667,27 @@ function RecordingHeroScreen({
641
667
  const innerWidth = Math.max(10, size.columns - 4);
642
668
  if (telemetry.status === "stopped") {
643
669
  const handoff = stoppedHandoffCopy(artifact, canTranscribe);
670
+ const phase = stoppedPhase(artifact);
644
671
  const meta3 = [
645
672
  telemetry.durationMs != null ? formatClockMs(telemetry.durationMs) : null,
646
673
  formatBytes2(telemetry.sizeBytes) || null
647
674
  ].filter(Boolean).join(" \xB7 ");
675
+ const saved = artifact?.uploadStatus === "uploaded" ? "\u2713 Saved to Recappi Cloud" : "\u2713 Saved to your Mac";
648
676
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
649
677
  /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "recappi \xB7 Recording" }),
650
678
  /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
651
- /* @__PURE__ */ jsx3(Text2, { color: "green", children: "\u2713 Saved to your Mac" }),
679
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: saved }),
652
680
  meta3 ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: meta3 }) : null,
653
681
  telemetry.savedPath ? /* @__PURE__ */ jsx3(Text2, { dimColor: true, wrap: "truncate-middle", children: telemetry.savedPath }) : null
654
682
  ] }),
683
+ phase ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, children: [
684
+ /* @__PURE__ */ jsx3(Text2, { color: "cyan", children: `\u25D0 ${phase.label}` }),
685
+ phase.fraction != null ? /* @__PURE__ */ jsxs2(Fragment, { children: [
686
+ /* @__PURE__ */ jsx3(Text2, { children: " " }),
687
+ /* @__PURE__ */ jsx3(ProgressBar, { fraction: phase.fraction }),
688
+ /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: ` ${Math.round(phase.fraction * 100)}%` })
689
+ ] }) : /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: "\u2026" })
690
+ ] }) : null,
655
691
  /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
656
692
  /* @__PURE__ */ jsx3(Text2, { color: handoff.tone === "red" ? "red" : handoff.tone === "green" ? "green" : void 0, dimColor: handoff.tone === "dim", children: handoff.text }),
657
693
  artifact?.error ? /* @__PURE__ */ jsx3(Text2, { color: "red", wrap: "truncate-end", children: artifact.error }) : null
@@ -668,24 +704,33 @@ function RecordingHeroScreen({
668
704
  const paused = telemetry.status === "paused";
669
705
  const starting = telemetry.status === "starting" || telemetry.status === "stopping";
670
706
  const badge = paused ? "\u23F8 PAUSED" : starting ? "\u2026" : "\u23FA REC";
707
+ const meterW = Math.max(10, Math.min(48, innerWidth - 22));
708
+ const sizeStr = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
709
+ const context = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, sizeStr || null].filter(Boolean).join(" \xB7 ");
671
710
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, height: size.rows, children: [
672
711
  /* @__PURE__ */ jsxs2(Text2, { children: [
673
712
  /* @__PURE__ */ jsx3(Text2, { bold: true, color: "green", children: "recappi" }),
674
713
  /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: " \xB7 Recording" })
675
714
  ] }),
676
- /* @__PURE__ */ jsxs2(Box2, { flexGrow: 1, flexDirection: "column", justifyContent: "center", alignItems: "center", children: [
677
- /* @__PURE__ */ jsx3(Text2, { bold: true, color: paused ? "yellow" : "red", children: badge }),
678
- /* @__PURE__ */ jsx3(Text2, { bold: true, children: elapsed }),
679
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: telemetry.level == null ? (
680
- // No level telemetry yet show honest activity, not a flat meter that
681
- // looks like silence (the elapsed timer above proves it's live).
715
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, paddingX: 1, flexGrow: 1, flexDirection: "column", children: [
716
+ /* @__PURE__ */ jsxs2(Text2, { children: [
717
+ /* @__PURE__ */ jsx3(Text2, { bold: true, color: paused ? "yellow" : "red", children: badge }),
718
+ /* @__PURE__ */ jsx3(Text2, { children: " " }),
719
+ /* @__PURE__ */ jsx3(Text2, { bold: true, children: elapsed })
720
+ ] }),
721
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, flexDirection: "column", children: telemetry.level == null ? (
722
+ // No level telemetry yet — honest activity, not a flat meter that
723
+ // reads as silence (the elapsed timer above proves it's live).
682
724
  /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: paused ? "Paused" : `Capturing audio${".".repeat(Math.floor(tick / 1e3) % 3 + 1)}` })
683
- ) : /* @__PURE__ */ jsx3(Text2, { color: paused ? "gray" : "red", children: waveform(wave, innerWidth) }) }),
684
- /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
685
- telemetry.sourceLabel,
686
- telemetry.micEnabled ? " + Microphone" : ""
725
+ ) : /* @__PURE__ */ jsxs2(Fragment, { children: [
726
+ /* @__PURE__ */ jsx3(MeterRow, { label: "System", samples: waveSys, level: telemetry.level.system ?? 0, paused, width: meterW }),
727
+ telemetry.micEnabled ? /* @__PURE__ */ jsx3(MeterRow, { label: "Mic", samples: waveMic, level: telemetry.level.mic ?? 0, paused, width: meterW }) : null
687
728
  ] }) }),
688
- captions ? /* @__PURE__ */ jsx3(Box2, { marginTop: 1, flexDirection: "column", alignItems: "center", width: innerWidth, children: /* @__PURE__ */ jsx3(HeroCaptions, { state: captions }) }) : null
729
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text2, { dimColor: true, children: context }) }),
730
+ captions ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
731
+ /* @__PURE__ */ jsx3(Text2, { bold: true, dimColor: true, children: "LIVE CAPTIONS" }),
732
+ /* @__PURE__ */ jsx3(HeroCaptions, { state: captions })
733
+ ] }) : null
689
734
  ] }),
690
735
  /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
691
736
  "q stop & save",
@@ -702,7 +747,7 @@ function HeroCaptions({ state }) {
702
747
  return /* @__PURE__ */ jsx3(Text2, { color: captionError ? "yellow" : void 0, dimColor: !captionError, wrap: "truncate-end", children: captionError ?? (state.status === "live" ? "Listening for speech\u2026" : liveCaptionStatusLabel(state.status)) });
703
748
  }
704
749
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
705
- recent.map((line) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", alignItems: "center", children: [
750
+ recent.map((line) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
706
751
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
707
752
  line.speaker ? `${line.speaker}: ` : "",
708
753
  line.text
@@ -715,11 +760,8 @@ function HeroCaptions({ state }) {
715
760
  ] });
716
761
  }
717
762
  function stoppedHandoffCopy(artifact, canTranscribe) {
718
- if (artifact?.uploadStatus === "uploading") {
719
- return { text: "Uploading\u2026", tone: "normal" };
720
- }
721
- if (artifact?.transcriptionStatus === "processing") {
722
- return { text: "Transcribing\u2026", tone: "normal" };
763
+ if (artifact?.uploadStatus === "uploading" || artifact?.transcriptionStatus === "processing") {
764
+ return { text: "esc run in background", tone: "dim" };
723
765
  }
724
766
  if (artifact?.transcriptionStatus === "queued") {
725
767
  return { text: "Transcription queued \xB7 \u23CE open recording \xB7 n not now", tone: "green" };
@@ -763,13 +805,16 @@ function AccountView({ status }) {
763
805
  function AccountBody({ status }) {
764
806
  return /* @__PURE__ */ jsxs3(Fragment2, { children: [
765
807
  /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
766
- /* @__PURE__ */ jsx5(Text3, { bold: true, color: "green", children: status.email ?? status.userId ?? "Signed in" }),
808
+ /* @__PURE__ */ jsxs3(Text3, { children: [
809
+ /* @__PURE__ */ jsx5(Text3, { color: "green", children: "\u25CF " }),
810
+ /* @__PURE__ */ jsx5(Text3, { bold: true, children: status.email ?? status.userId ?? "Signed in" })
811
+ ] }),
767
812
  status.email && status.userId ? /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: status.userId }) : null,
768
813
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: `origin ${status.origin}` })
769
814
  ] }),
770
815
  status.billing ? /* @__PURE__ */ jsx5(Usage, { billing: status.billing }) : null,
771
816
  /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
772
- /* @__PURE__ */ jsx5(Text3, { bold: true, children: "Local store" }),
817
+ /* @__PURE__ */ jsx5(Text3, { bold: true, dimColor: true, children: "LOCAL STORE" }),
773
818
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, wrap: "truncate-middle", children: status.localStore.path }),
774
819
  /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
775
820
  `${status.localStore.accountScopedArtifacts} artifact${status.localStore.accountScopedArtifacts === 1 ? "" : "s"} for this account`,
@@ -783,6 +828,7 @@ function Usage({ billing }) {
783
828
  const minutesUsed = billing.minutesUsed;
784
829
  const storageCap = billing.storageCapBytes;
785
830
  return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
831
+ /* @__PURE__ */ jsx5(Text3, { bold: true, dimColor: true, children: "USAGE" }),
786
832
  /* @__PURE__ */ jsxs3(Text3, { children: [
787
833
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Plan " }),
788
834
  /* @__PURE__ */ jsx5(Text3, { bold: true, children: billing.tier })
@@ -883,9 +929,10 @@ function JobRow({
883
929
  const glyph = statusGlyph(item.status, spinnerFrame);
884
930
  const title = item.recording?.title ?? item.recordingId;
885
931
  return /* @__PURE__ */ jsxs5(Box5, { children: [
886
- /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8 " : " " }),
887
- /* @__PURE__ */ jsx7(Text5, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
888
- /* @__PURE__ */ jsx7(Text5, { bold: selected, children: padCell(title, 24) }),
932
+ /* @__PURE__ */ jsx7(Box5, { width: 3, children: /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
933
+ /* @__PURE__ */ jsx7(Box5, { width: 2, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: glyph }) }),
934
+ /* @__PURE__ */ jsx7(Box5, { width: 13, children: /* @__PURE__ */ jsx7(Text5, { color: style.color, children: style.label }) }),
935
+ /* @__PURE__ */ jsx7(Box5, { width: 26, children: /* @__PURE__ */ jsx7(Text5, { bold: selected, wrap: "truncate-end", children: title }) }),
889
936
  /* @__PURE__ */ jsx7(Text5, { dimColor: !selected, children: jobDetail(item) })
890
937
  ] });
891
938
  }
@@ -946,7 +993,7 @@ function recordingLayout(columns) {
946
993
  const showWhen = usable >= 54;
947
994
  const title = Math.max(
948
995
  10,
949
- usable - MARKER_W - GLYPH_W - LENGTH_W - (showWhen ? WHEN_W : 0)
996
+ usable - MARKER_W - GLYPH_W - LENGTH_W - (showWhen ? WHEN_W : 0) - DL_W
950
997
  );
951
998
  return { title, showWhen };
952
999
  }
@@ -963,33 +1010,25 @@ function RecordingRow({
963
1010
  const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
964
1011
  const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
965
1012
  return /* @__PURE__ */ jsxs6(Box7, { children: [
966
- /* @__PURE__ */ jsx9(Text7, { color: "cyan", children: selected ? "\u25B8 " : " " }),
967
- /* @__PURE__ */ jsx9(Text7, { color, children: `${glyph} ` }),
968
- /* @__PURE__ */ jsx9(Text7, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
969
- /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
970
- showWhen ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: padDisplay(formatAge(item.createdAt, nowMs), WHEN_W) }) : null,
971
- /* @__PURE__ */ jsx9(Text7, { color: "green", children: downloaded ? " \u2913" : " " })
972
- ] });
973
- }
974
- function RecordingHeader({ columns }) {
975
- const { title, showWhen } = recordingLayout(columns);
976
- return /* @__PURE__ */ jsxs6(Box7, { children: [
977
- /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
978
- /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: padDisplay("TITLE", title) }),
979
- /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
980
- showWhen ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "WHEN" }) : null
1013
+ /* @__PURE__ */ jsx9(Box7, { width: MARKER_W, children: /* @__PURE__ */ jsx9(Text7, { color: "cyan", children: selected ? "\u25B8" : "" }) }),
1014
+ /* @__PURE__ */ jsx9(Box7, { width: GLYPH_W, children: /* @__PURE__ */ jsx9(Text7, { color, children: glyph }) }),
1015
+ /* @__PURE__ */ jsx9(Box7, { width: title, children: /* @__PURE__ */ jsx9(Text7, { bold: selected, wrap: "truncate-end", children: recordingTitle2(item) }) }),
1016
+ /* @__PURE__ */ jsx9(Box7, { width: LENGTH_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: duration3 }) }),
1017
+ showWhen ? /* @__PURE__ */ jsx9(Box7, { width: WHEN_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: formatAge(item.createdAt, nowMs) }) }) : null,
1018
+ /* @__PURE__ */ jsx9(Box7, { width: DL_W, justifyContent: "flex-end", children: /* @__PURE__ */ jsx9(Text7, { color: "green", children: downloaded ? "\u2913" : "" }) })
981
1019
  ] });
982
1020
  }
983
- var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W;
1021
+ var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W, DL_W;
984
1022
  var init_RecordingRow = __esm({
985
1023
  "src/tui/RecordingRow.tsx"() {
986
1024
  "use strict";
987
1025
  init_format();
988
1026
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
989
- MARKER_W = 2;
1027
+ MARKER_W = 3;
990
1028
  GLYPH_W = 2;
991
- LENGTH_W = 9;
1029
+ LENGTH_W = 8;
992
1030
  WHEN_W = 9;
1031
+ DL_W = 3;
993
1032
  }
994
1033
  });
995
1034
 
@@ -1009,28 +1048,25 @@ function RecordingsView({
1009
1048
  if (items.length === 0) {
1010
1049
  return /* @__PURE__ */ jsx10(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
1011
1050
  }
1012
- return /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
1013
- /* @__PURE__ */ jsx10(RecordingHeader, { columns }),
1014
- items.map((item, index) => {
1015
- const bucket = dateBucket(item.createdAt, nowMs);
1016
- const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
1017
- return /* @__PURE__ */ jsxs7(React5.Fragment, { children: [
1018
- showHeader ? /* @__PURE__ */ jsx10(Box8, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx10(Text8, { bold: true, color: "blue", children: bucket }) }) : null,
1019
- /* @__PURE__ */ jsx10(
1020
- RecordingRow,
1021
- {
1022
- item,
1023
- selected: index === selectedIndex,
1024
- nowMs,
1025
- columns,
1026
- jobStatus: jobStatusByRecording?.get(item.recordingId),
1027
- downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
1028
- spinnerFrame
1029
- }
1030
- )
1031
- ] }, item.recordingId);
1032
- })
1033
- ] });
1051
+ return /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => {
1052
+ const bucket = dateBucket(item.createdAt, nowMs);
1053
+ const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
1054
+ return /* @__PURE__ */ jsxs7(React5.Fragment, { children: [
1055
+ showHeader ? /* @__PURE__ */ jsx10(Box8, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx10(Text8, { bold: true, dimColor: true, children: bucket }) }) : null,
1056
+ /* @__PURE__ */ jsx10(
1057
+ RecordingRow,
1058
+ {
1059
+ item,
1060
+ selected: index === selectedIndex,
1061
+ nowMs,
1062
+ columns,
1063
+ jobStatus: jobStatusByRecording?.get(item.recordingId),
1064
+ downloaded: downloadedRecordingIds?.has(item.recordingId) ?? false,
1065
+ spinnerFrame
1066
+ }
1067
+ )
1068
+ ] }, item.recordingId);
1069
+ }) });
1034
1070
  }
1035
1071
  var init_RecordingsView = __esm({
1036
1072
  "src/tui/RecordingsView.tsx"() {
@@ -1060,15 +1096,16 @@ function PeekBody({
1060
1096
  const meta3 = [
1061
1097
  item.durationMs ? formatClockMs(item.durationMs) : null,
1062
1098
  formatBytes2(item.sizeBytes) || null,
1063
- item.contentType || null
1064
- ].filter(Boolean).join(" \xB7 ");
1099
+ formatAge(item.createdAt, nowMs)
1100
+ ].filter(Boolean).join(" \xB7 ");
1065
1101
  return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1066
- /* @__PURE__ */ jsx11(Text9, { bold: true, color: "green", wrap: "truncate-end", children: recordingTitle2(item) }),
1067
- /* @__PURE__ */ jsx11(Text9, { color: style.color, children: `${style.glyph} ${style.label}` }),
1068
- meta3 ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: meta3 }) : null,
1069
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
1102
+ /* @__PURE__ */ jsx11(Text9, { bold: true, wrap: "truncate-end", children: recordingTitle2(item) }),
1103
+ /* @__PURE__ */ jsxs8(Box9, { children: [
1104
+ /* @__PURE__ */ jsx11(Text9, { color: style.color, children: `${style.glyph} ${style.label}` }),
1105
+ meta3 ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${meta3}` }) : null
1106
+ ] }),
1070
1107
  /* @__PURE__ */ jsx11(Box9, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(SummarySection, { item, summary }) }),
1071
- /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
1108
+ /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "\u23CE open \xB7 t transcript" }) })
1072
1109
  ] });
1073
1110
  }
1074
1111
  function SummarySection({
@@ -1083,7 +1120,7 @@ function SummarySection({
1083
1120
  }
1084
1121
  const points = (summary.keyPoints ?? []).slice(0, 3);
1085
1122
  return /* @__PURE__ */ jsxs8(Fragment3, { children: [
1086
- /* @__PURE__ */ jsx11(Text9, { bold: true, children: "Summary" }),
1123
+ /* @__PURE__ */ jsx11(Text9, { bold: true, dimColor: true, children: "SUMMARY" }),
1087
1124
  /* @__PURE__ */ jsx11(Text9, { children: summary.tldr }),
1088
1125
  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
1089
1126
  ] });
@@ -1098,7 +1135,7 @@ var init_RecordingPeek = __esm({
1098
1135
 
1099
1136
  // src/tui/OverviewView.tsx
1100
1137
  import { Box as Box10, Text as Text10 } from "ink";
1101
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1138
+ import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1102
1139
  function OverviewView({
1103
1140
  recordings,
1104
1141
  jobs,
@@ -1119,12 +1156,21 @@ function OverviewView({
1119
1156
  const queued = stats?.jobs.queued ?? jobCounts.queued;
1120
1157
  return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", children: [
1121
1158
  /* @__PURE__ */ jsxs9(Box10, { children: [
1122
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Recordings " }),
1123
1159
  /* @__PURE__ */ jsx12(Text10, { bold: true, children: stats?.recordings.total ?? recordings.length }),
1124
- stats?.recordings.ready != null ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
1125
- stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
1126
- running > 0 ? /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
1127
- queued > 0 ? /* @__PURE__ */ jsx12(Text10, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
1160
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " recordings" }),
1161
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1162
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
1163
+ /* @__PURE__ */ jsx12(Text10, { color: "green", children: `${stats.recordings.ready} ready` })
1164
+ ] }) : null,
1165
+ running > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1166
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
1167
+ /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: `${running} transcribing` })
1168
+ ] }) : null,
1169
+ queued > 0 ? /* @__PURE__ */ jsxs9(Fragment4, { children: [
1170
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
1171
+ /* @__PURE__ */ jsx12(Text10, { color: "yellow", children: `${queued} queued` })
1172
+ ] }) : null,
1173
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null
1128
1174
  ] }),
1129
1175
  /* @__PURE__ */ jsxs9(Box10, { flexDirection: "row", alignItems: "flex-start", children: [
1130
1176
  /* @__PURE__ */ jsx12(Box10, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx12(
@@ -1133,7 +1179,7 @@ function OverviewView({
1133
1179
  items: recordings,
1134
1180
  selectedIndex,
1135
1181
  nowMs,
1136
- columns,
1182
+ columns: showPeek ? Math.max(20, columns - peekWidth - 1) : columns,
1137
1183
  jobStatusByRecording,
1138
1184
  downloadedRecordingIds,
1139
1185
  spinnerFrame
@@ -1295,7 +1341,7 @@ var init_JobDetailView = __esm({
1295
1341
  // src/tui/RecordingDetailView.tsx
1296
1342
  import React6, { useMemo as useMemo2, useState as useState4 } from "react";
1297
1343
  import { Box as Box12, Text as Text12, useInput as useInput3 } from "ink";
1298
- import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1344
+ import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1299
1345
  function RecordingDetailView({
1300
1346
  item,
1301
1347
  nowMs,
@@ -1360,14 +1406,14 @@ function RecordingDetailView({
1360
1406
  });
1361
1407
  return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1362
1408
  /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "\u2039 Recordings" }),
1363
- /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { bold: true, color: "green", children: title }) }),
1409
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { bold: true, children: title }) }),
1364
1410
  /* @__PURE__ */ jsxs11(Text12, { children: [
1365
1411
  /* @__PURE__ */ jsx14(Text12, { color: style.color, children: `${style.glyph} ${style.label}` }),
1366
1412
  /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
1367
1413
  ] }),
1368
1414
  meta3 ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: meta3 }) : null,
1369
1415
  /* @__PURE__ */ jsx14(AudioActionRow, { item, audio }),
1370
- !item.activeTranscriptId ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs11(Fragment4, { children: [
1416
+ !item.activeTranscriptId ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs11(Fragment5, { children: [
1371
1417
  /* @__PURE__ */ jsx14(TabBar, { active: tab }),
1372
1418
  /* @__PURE__ */ jsx14(Box12, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx14(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx14(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx14(TranscriptPane, { segments, win: segWin }) })
1373
1419
  ] }),
@@ -1386,7 +1432,7 @@ function RecordingDetailView({
1386
1432
  function TabBar({ active }) {
1387
1433
  return /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: TAB_ORDER.map((tab, i) => /* @__PURE__ */ jsxs11(React6.Fragment, { children: [
1388
1434
  i > 0 ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: " " }) : null,
1389
- tab === active ? /* @__PURE__ */ jsx14(Text12, { inverse: true, bold: true, children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
1435
+ tab === active ? /* @__PURE__ */ jsx14(Text12, { inverse: true, bold: true, color: "cyan", children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
1390
1436
  ] }, tab)) });
1391
1437
  }
1392
1438
  function AudioActionRow({
@@ -1432,7 +1478,7 @@ function SummaryPane({
1432
1478
  return /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` });
1433
1479
  }
1434
1480
  const points = (summary.keyPoints ?? []).slice(0, Math.max(1, budget - 4));
1435
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1481
+ return /* @__PURE__ */ jsxs11(Fragment5, { children: [
1436
1482
  /* @__PURE__ */ jsx14(Text12, { children: summary.tldr }),
1437
1483
  points.length > 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: `\u2022 ${point}` }, i)) }) : null
1438
1484
  ] });
@@ -1443,13 +1489,13 @@ function ChaptersPane({
1443
1489
  selectedIndex
1444
1490
  }) {
1445
1491
  if (chapters.length === 0) return /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "No chapters" });
1446
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1492
+ return /* @__PURE__ */ jsxs11(Fragment5, { children: [
1447
1493
  chapters.slice(win.start, win.end).map((chapter, i) => {
1448
1494
  const index = win.start + i;
1449
1495
  const selected = index === selectedIndex;
1450
1496
  return /* @__PURE__ */ jsxs11(Text12, { wrap: "truncate-end", children: [
1451
1497
  /* @__PURE__ */ jsx14(Text12, { color: "cyan", children: selected ? "\u25B8 " : " " }),
1452
- /* @__PURE__ */ jsx14(Text12, { color: "blue", children: `[${formatClockMs(chapter.startMs)}] ` }),
1498
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: `[${formatClockMs(chapter.startMs)}] ` }),
1453
1499
  /* @__PURE__ */ jsx14(Text12, { bold: selected, children: chapter.title })
1454
1500
  ] }, index);
1455
1501
  }),
@@ -1461,10 +1507,10 @@ function TranscriptPane({
1461
1507
  win
1462
1508
  }) {
1463
1509
  if (segments.length === 0) return /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "(no segments)" });
1464
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
1510
+ return /* @__PURE__ */ jsxs11(Fragment5, { children: [
1465
1511
  segments.slice(win.start, win.end).map((seg, i) => /* @__PURE__ */ jsxs11(Text12, { children: [
1466
- /* @__PURE__ */ jsx14(Text12, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
1467
- seg.speaker ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: `${seg.speaker} ` }) : null,
1512
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: `[${formatClockMs(seg.startMs)}] ` }),
1513
+ seg.speaker ? /* @__PURE__ */ jsx14(Text12, { color: "cyan", children: `${seg.speaker} ` }) : null,
1468
1514
  /* @__PURE__ */ jsx14(Text12, { children: seg.text })
1469
1515
  ] }, win.start + i)),
1470
1516
  win.maxScroll > 0 ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ` ${win.start + 1}\u2013${win.end} / ${segments.length}` }) : null
@@ -1584,7 +1630,7 @@ function PermissionPreflightView({
1584
1630
  return /* @__PURE__ */ jsxs13(Box14, { flexDirection: "column", paddingX: 1, children: [
1585
1631
  /* @__PURE__ */ jsxs13(Text14, { children: [
1586
1632
  /* @__PURE__ */ jsx16(Text14, { dimColor: true, children: "\u2039 " }),
1587
- /* @__PURE__ */ jsx16(Text14, { bold: true, color: "cyan", children: "Recording permissions" })
1633
+ /* @__PURE__ */ jsx16(Text14, { bold: true, children: "Recording permissions" })
1588
1634
  ] }),
1589
1635
  /* @__PURE__ */ jsx16(Box14, { marginTop: 1, flexDirection: "column", children: items.length === 0 ? /* @__PURE__ */ jsx16(Text14, { dimColor: true, children: "Checking permissions\u2026" }) : items.map((item) => {
1590
1636
  const status = statusGlyph2(item.status);
@@ -1631,60 +1677,101 @@ var init_PermissionPreflightView = __esm({
1631
1677
  });
1632
1678
 
1633
1679
  // src/tui/RecordSetupView.tsx
1634
- import { useState as useState6 } from "react";
1680
+ import { useEffect as useEffect3, useRef, useState as useState6 } from "react";
1635
1681
  import { Box as Box15, Text as Text15, useInput as useInput5 } from "ink";
1636
1682
  import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1683
+ function levelDb2(level) {
1684
+ if (level <= 0.03) return "silent";
1685
+ return `${Math.round(level * 60 - 60)} dB`;
1686
+ }
1687
+ function meterBar(level, width) {
1688
+ const f = Math.max(0, Math.min(1, level));
1689
+ const filled = Math.round(f * width);
1690
+ return "\u2587".repeat(filled) + "\u2591".repeat(Math.max(0, width - filled));
1691
+ }
1692
+ function InputMeter({ level }) {
1693
+ if (level == null) return /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "\u2014" });
1694
+ const silent = level <= 0.03;
1695
+ return /* @__PURE__ */ jsxs14(Text15, { children: [
1696
+ /* @__PURE__ */ jsx17(Text15, { color: silent ? "yellow" : "cyan", children: meterBar(level, METER_W) }),
1697
+ /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: ` ${levelDb2(level)}` })
1698
+ ] });
1699
+ }
1637
1700
  function RecordSetupView({
1638
1701
  model,
1702
+ levels,
1639
1703
  onStart,
1640
- onCancel
1704
+ onCancel,
1705
+ onSelectionChange
1641
1706
  }) {
1642
- const size = useTerminalSize();
1643
1707
  const [srcIdx, setSrcIdx] = useState6(0);
1644
1708
  const [includeMic, setIncludeMic] = useState6(true);
1645
1709
  const [micIdx, setMicIdx] = useState6(
1646
1710
  () => Math.max(0, model.microphones?.findIndex((device) => device.isDefault) ?? 0)
1647
1711
  );
1648
1712
  const [sceneIdx, setSceneIdx] = useState6(0);
1713
+ const userPickedMic = useRef(false);
1649
1714
  const sources = model.sources;
1650
1715
  const microphones = model.microphones ?? [];
1651
1716
  const selected = sources[Math.min(srcIdx, Math.max(0, sources.length - 1))];
1652
1717
  const selectedMic = microphones[Math.min(micIdx, Math.max(0, microphones.length - 1))];
1653
- const wide = size.columns >= 100;
1654
1718
  const hasAppSource = sources.some((source) => source.kind === "app");
1655
1719
  const hasMultipleSources = sources.length > 1;
1656
1720
  const hasMultipleMicrophones = microphones.length > 1;
1721
+ const selection = {
1722
+ sourceId: selected?.id ?? "system",
1723
+ includeMicrophone: includeMic,
1724
+ ...includeMic && selectedMic ? { microphoneDeviceId: selectedMic.id } : {},
1725
+ sceneId: model.scenes[sceneIdx]?.id
1726
+ };
1727
+ useEffect3(() => {
1728
+ onSelectionChange?.(selection);
1729
+ }, [
1730
+ includeMic,
1731
+ onSelectionChange,
1732
+ selection.includeMicrophone,
1733
+ selection.microphoneDeviceId,
1734
+ selection.sceneId,
1735
+ selection.sourceId,
1736
+ srcIdx,
1737
+ micIdx
1738
+ ]);
1739
+ useEffect3(() => {
1740
+ if (userPickedMic.current || microphones.length === 0) return;
1741
+ const di = microphones.findIndex((device) => device.isDefault);
1742
+ if (di > 0) setMicIdx(di);
1743
+ }, [microphones.length]);
1657
1744
  useInput5((input, key) => {
1658
1745
  if (key.upArrow || input === "k") setSrcIdx((i) => Math.max(0, i - 1));
1659
1746
  else if (key.downArrow || input === "j") setSrcIdx((i) => Math.min(sources.length - 1, i + 1));
1660
1747
  else if (input === " ") setIncludeMic((m) => !m);
1661
- else if (input === "m" && includeMic && hasMultipleMicrophones)
1748
+ else if (input === "m" && includeMic && hasMultipleMicrophones) {
1749
+ userPickedMic.current = true;
1662
1750
  setMicIdx((i) => (i + 1) % microphones.length);
1663
- else if (input === "s" && model.scenes.length > 1) setSceneIdx((i) => (i + 1) % model.scenes.length);
1664
- else if (key.return && selected) {
1665
- onStart({
1666
- sourceId: selected.id,
1667
- includeMicrophone: includeMic,
1668
- ...includeMic && selectedMic ? { microphoneDeviceId: selectedMic.id } : {},
1669
- sceneId: model.scenes[sceneIdx]?.id
1670
- });
1671
- } else if (key.escape) onCancel();
1751
+ } else if (input === "s" && model.scenes.length > 1) setSceneIdx((i) => (i + 1) % model.scenes.length);
1752
+ else if (key.return && selected) onStart(selection);
1753
+ else if (key.escape) onCancel();
1672
1754
  });
1673
1755
  const sourceList = /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", children: [
1674
- /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "SOURCE" }),
1756
+ /* @__PURE__ */ jsxs14(Box15, { children: [
1757
+ /* @__PURE__ */ jsx17(Box15, { width: 36, children: /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "SOURCE" }) }),
1758
+ /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "INPUT" })
1759
+ ] }),
1675
1760
  sources.map((s, i) => {
1676
1761
  const on = i === srcIdx;
1677
- return /* @__PURE__ */ jsxs14(Text15, { color: on ? "cyan" : void 0, wrap: "truncate-end", children: [
1678
- on ? "\u25B8 \u25CF " : " \u25CB ",
1679
- s.label
1762
+ return /* @__PURE__ */ jsxs14(Box15, { children: [
1763
+ /* @__PURE__ */ jsx17(Box15, { width: 36, children: /* @__PURE__ */ jsxs14(Text15, { color: on ? "cyan" : void 0, wrap: "truncate-end", children: [
1764
+ on ? "\u25B8 \u25CF " : " \u25CB ",
1765
+ s.label
1766
+ ] }) }),
1767
+ /* @__PURE__ */ jsx17(InputMeter, { level: levels?.bySourceId?.[s.id] })
1680
1768
  ] }, s.id);
1681
1769
  }),
1682
1770
  !hasAppSource ? /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "No app-specific sources available right now" }) : null
1683
1771
  ] });
1684
- const capturePlan = /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", children: [
1685
- /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "CAPTURE PLAN" }),
1686
- /* @__PURE__ */ jsx17(Text15, { children: includeMic ? `${selected?.label ?? "System audio"} + microphone` : `${selected?.label ?? "System audio"} only` }),
1687
- /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: includeMic ? "Mic is mixed into the recording" : "Mic stays muted" })
1772
+ const capturePlan = /* @__PURE__ */ jsxs14(Text15, { children: [
1773
+ /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "Capture " }),
1774
+ /* @__PURE__ */ jsx17(Text15, { children: includeMic ? `${selected?.label ?? "System audio"} + microphone` : `${selected?.label ?? "System audio"} only` })
1688
1775
  ] });
1689
1776
  const shortcuts = [
1690
1777
  hasMultipleSources ? "\u2191\u2193 source" : void 0,
@@ -1696,9 +1783,9 @@ function RecordSetupView({
1696
1783
  ].filter(Boolean).join(" \xB7 ");
1697
1784
  return /* @__PURE__ */ jsxs14(Box15, { flexDirection: "column", paddingX: 1, children: [
1698
1785
  /* @__PURE__ */ jsx17(Text15, { bold: true, color: "green", children: "New recording" }),
1699
- /* @__PURE__ */ jsxs14(Box15, { marginTop: 1, flexDirection: wide ? "row" : "column", children: [
1700
- /* @__PURE__ */ jsx17(Box15, { flexGrow: 1, flexDirection: "column", children: sourceList }),
1701
- /* @__PURE__ */ jsx17(Box15, { marginLeft: wide ? 4 : 0, marginTop: wide ? 0 : 1, children: capturePlan })
1786
+ /* @__PURE__ */ jsxs14(Box15, { marginTop: 1, flexDirection: "column", children: [
1787
+ sourceList,
1788
+ /* @__PURE__ */ jsx17(Box15, { marginTop: 1, children: capturePlan })
1702
1789
  ] }),
1703
1790
  /* @__PURE__ */ jsxs14(Box15, { marginTop: 1, flexDirection: "column", children: [
1704
1791
  /* @__PURE__ */ jsxs14(Text15, { children: [
@@ -1706,12 +1793,14 @@ function RecordSetupView({
1706
1793
  /* @__PURE__ */ jsx17(Text15, { color: includeMic ? "green" : "gray", children: includeMic ? "[x] include mic" : "[ ] include mic" }),
1707
1794
  /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: " (space)" })
1708
1795
  ] }),
1709
- selectedMic ? /* @__PURE__ */ jsxs14(Text15, { dimColor: !includeMic, children: [
1710
- /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "Mic device " }),
1711
- /* @__PURE__ */ jsx17(Text15, { children: selectedMic.label }),
1712
- selectedMic.isDefault ? /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: " \xB7 default" }) : null,
1713
- includeMic && hasMultipleMicrophones ? /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: " (m to change)" }) : null
1796
+ selectedMic ? /* @__PURE__ */ jsxs14(Box15, { children: [
1797
+ /* @__PURE__ */ jsx17(Box15, { width: 36, children: /* @__PURE__ */ jsxs14(Text15, { dimColor: !includeMic, wrap: "truncate-end", children: [
1798
+ /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "Mic device " }),
1799
+ selectedMic.label
1800
+ ] }) }),
1801
+ includeMic ? /* @__PURE__ */ jsx17(InputMeter, { level: levels?.byMicrophoneId?.[selectedMic.id] }) : null
1714
1802
  ] }) : null,
1803
+ includeMic && hasMultipleMicrophones ? /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: " (m to change device)" }) : null,
1715
1804
  model.scenes.length > 0 ? /* @__PURE__ */ jsxs14(Text15, { children: [
1716
1805
  /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: "Scene " }),
1717
1806
  /* @__PURE__ */ jsx17(Text15, { children: model.scenes[sceneIdx]?.label ?? "Default" }),
@@ -1721,17 +1810,18 @@ function RecordSetupView({
1721
1810
  /* @__PURE__ */ jsx17(Box15, { marginTop: 1, children: /* @__PURE__ */ jsx17(Text15, { dimColor: true, children: shortcuts }) })
1722
1811
  ] });
1723
1812
  }
1813
+ var METER_W;
1724
1814
  var init_RecordSetupView = __esm({
1725
1815
  "src/tui/RecordSetupView.tsx"() {
1726
1816
  "use strict";
1727
- init_terminal();
1817
+ METER_W = 12;
1728
1818
  }
1729
1819
  });
1730
1820
 
1731
1821
  // src/tui/AppShell.tsx
1732
- import { useCallback, useEffect as useEffect3, useState as useState7 } from "react";
1822
+ import { useCallback, useEffect as useEffect4, useState as useState7 } from "react";
1733
1823
  import { Box as Box16, Text as Text16, useApp, useInput as useInput6 } from "ink";
1734
- import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1824
+ import { Fragment as Fragment6, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1735
1825
  function recordErrorCopy(code, message) {
1736
1826
  switch (code) {
1737
1827
  case "record.helper_unavailable":
@@ -1812,6 +1902,43 @@ function transcribeHandoffErrorCopy(error51) {
1812
1902
  return "Could not start transcription. Please try again.";
1813
1903
  }
1814
1904
  }
1905
+ function artifactProgressPatchFromOperationEvent(event) {
1906
+ if (event.command !== "upload") return void 0;
1907
+ if (event.status === "uploading" && typeof event.percent === "number") {
1908
+ return {
1909
+ uploadStatus: "uploading",
1910
+ uploadProgress: Math.max(0, Math.min(1, event.percent / 100))
1911
+ };
1912
+ }
1913
+ if (event.status === "finishing_upload") {
1914
+ return {
1915
+ uploadStatus: "uploading",
1916
+ uploadProgress: 1
1917
+ };
1918
+ }
1919
+ if (event.status === "starting_transcription") {
1920
+ return {
1921
+ uploadStatus: "uploaded",
1922
+ uploadProgress: 1,
1923
+ transcriptionStatus: "queued"
1924
+ };
1925
+ }
1926
+ return void 0;
1927
+ }
1928
+ function transcriptionStatusFromJob(status) {
1929
+ switch (status) {
1930
+ case "queued":
1931
+ return "queued";
1932
+ case "running":
1933
+ return "processing";
1934
+ case "succeeded":
1935
+ return "ready";
1936
+ case "failed":
1937
+ return "failed";
1938
+ default:
1939
+ return "not_started";
1940
+ }
1941
+ }
1815
1942
  function permissionItemsFromRecordError(data) {
1816
1943
  const sidecarError = isRecord8(data) ? data : void 0;
1817
1944
  const sidecarData = isRecord8(sidecarError?.data) ? sidecarError.data : void 0;
@@ -1850,6 +1977,7 @@ function AppShell({
1850
1977
  listDownloadedRecordingIds,
1851
1978
  fetchRecordSetup,
1852
1979
  startLiveRecord,
1980
+ startRecordSetupPreview,
1853
1981
  transcribeRecordingArtifact,
1854
1982
  initialView = "overview",
1855
1983
  openUrl: openUrl2,
@@ -1884,6 +2012,13 @@ function AppShell({
1884
2012
  sources: DEFAULT_RECORDING_SOURCES,
1885
2013
  microphones: []
1886
2014
  });
2015
+ const [recordSetupSelection, setRecordSetupSelection] = useState7(
2016
+ DEFAULT_RECORDING_SELECTION
2017
+ );
2018
+ const [recordSetupLevels, setRecordSetupLevels] = useState7({
2019
+ bySourceId: {},
2020
+ byMicrophoneId: {}
2021
+ });
1887
2022
  const recordSetupModel = {
1888
2023
  sources: recordSetupInputs.sources.length > 0 ? recordSetupInputs.sources : DEFAULT_RECORDING_SOURCES,
1889
2024
  microphones: recordSetupInputs.microphones ?? [],
@@ -1896,10 +2031,58 @@ function AppShell({
1896
2031
  } catch {
1897
2032
  }
1898
2033
  }, [listDownloadedRecordingIds]);
1899
- useEffect3(() => {
2034
+ useEffect4(() => {
1900
2035
  void refreshDownloadedIds();
1901
2036
  }, [refreshDownloadedIds]);
1902
2037
  const screen = stack[stack.length - 1];
2038
+ useEffect4(() => {
2039
+ if (screen.kind !== "recordSetup" || !startRecordSetupPreview) return;
2040
+ let cancelled = false;
2041
+ let preview;
2042
+ let unsubscribe;
2043
+ const selection = recordSetupSelection;
2044
+ setRecordSetupLevels({ bySourceId: {}, byMicrophoneId: {} });
2045
+ startRecordSetupPreview(selection, recordSetupModel.sources).then((session) => {
2046
+ if (cancelled) {
2047
+ void session.stop();
2048
+ return;
2049
+ }
2050
+ preview = session;
2051
+ unsubscribe = session.source.onEvent((event) => {
2052
+ if (event.type !== "audio.level") return;
2053
+ const level = levelFromRmsDb(event.rmsDb);
2054
+ if (event.input === "microphone") {
2055
+ const microphones = recordSetupModel.microphones ?? [];
2056
+ const microphoneId = event.microphoneDeviceId ?? selection.microphoneDeviceId ?? microphones.find((device) => device.isDefault)?.id ?? microphones[0]?.id;
2057
+ if (!microphoneId) return;
2058
+ setRecordSetupLevels((current) => ({
2059
+ ...current,
2060
+ byMicrophoneId: { ...current.byMicrophoneId ?? {}, [microphoneId]: level }
2061
+ }));
2062
+ return;
2063
+ }
2064
+ const sourceId = event.sourceId ?? selection.sourceId;
2065
+ setRecordSetupLevels((current) => ({
2066
+ ...current,
2067
+ bySourceId: { ...current.bySourceId, [sourceId]: level }
2068
+ }));
2069
+ });
2070
+ }).catch(() => {
2071
+ });
2072
+ return () => {
2073
+ cancelled = true;
2074
+ unsubscribe?.();
2075
+ if (preview) void preview.stop();
2076
+ };
2077
+ }, [
2078
+ recordSetupSelection.includeMicrophone,
2079
+ recordSetupSelection.microphoneDeviceId,
2080
+ recordSetupSelection.sourceId,
2081
+ recordSetupModel.microphones,
2082
+ recordSetupModel.sources,
2083
+ screen.kind,
2084
+ startRecordSetupPreview
2085
+ ]);
1903
2086
  const beginLiveRecord = useCallback(
1904
2087
  (selection = DEFAULT_RECORDING_SELECTION) => {
1905
2088
  const capture = recordingCaptureMappingFromSelection(selection, recordSetupModel.sources);
@@ -1979,7 +2162,7 @@ function AppShell({
1979
2162
  setStack([{ kind: "overview" }]);
1980
2163
  }, [liveRecord, now, refreshDownloadedIds]);
1981
2164
  const liveSession = liveRecord?.kind === "live" ? liveRecord.session : void 0;
1982
- useEffect3(() => {
2165
+ useEffect4(() => {
1983
2166
  if (!liveSession) return;
1984
2167
  const session = liveSession;
1985
2168
  const unsubscribe = session.source.onEvent((event) => {
@@ -2002,7 +2185,7 @@ function AppShell({
2002
2185
  }, [liveSession]);
2003
2186
  const selectedRecording = screen.kind === "overview" ? recordings[selected] : void 0;
2004
2187
  const peekTranscriptId = selectedRecording?.activeTranscriptId ?? void 0;
2005
- useEffect3(() => {
2188
+ useEffect4(() => {
2006
2189
  if (!peekTranscriptId || summaryCache.has(peekTranscriptId)) return;
2007
2190
  let cancelled = false;
2008
2191
  const timer = setTimeout(() => {
@@ -2067,23 +2250,46 @@ function AppShell({
2067
2250
  artifact: {
2068
2251
  ...artifact,
2069
2252
  uploadStatus: "uploading",
2253
+ uploadProgress: 0,
2070
2254
  transcriptionStatus: "not_started",
2255
+ transcriptionProgress: void 0,
2071
2256
  error: void 0
2072
2257
  }
2073
2258
  });
2074
2259
  try {
2075
- const uploaded = await transcribeRecordingArtifact(artifact);
2260
+ const uploaded = await transcribeRecordingArtifact(artifact, (event) => {
2261
+ const patch = artifactProgressPatchFromOperationEvent(event);
2262
+ if (!patch) return;
2263
+ setLiveRecord((latest) => {
2264
+ if (latest?.kind !== "stopped" || latest.artifact?.sessionId !== artifact.sessionId) {
2265
+ return latest;
2266
+ }
2267
+ return {
2268
+ ...latest,
2269
+ artifact: {
2270
+ ...latest.artifact,
2271
+ ...patch
2272
+ }
2273
+ };
2274
+ });
2275
+ });
2076
2276
  const transcriptionStatus = uploaded.transcriptId != null || uploaded.status === "succeeded" || uploaded.status === "ready" ? "ready" : uploaded.status === "running" ? "processing" : uploaded.jobId ? "queued" : "not_started";
2077
- setLiveRecord({
2078
- ...current,
2079
- artifact: {
2080
- ...artifact,
2081
- recordingId: uploaded.recordingId,
2082
- ...uploaded.jobId ? { jobId: uploaded.jobId } : {},
2083
- ...uploaded.transcriptId ? { transcriptId: uploaded.transcriptId } : {},
2084
- uploadStatus: "uploaded",
2085
- transcriptionStatus
2086
- }
2277
+ setLiveRecord((latest) => {
2278
+ const base = latest?.kind === "stopped" && latest.artifact?.sessionId === artifact.sessionId ? latest : current;
2279
+ return {
2280
+ ...base,
2281
+ artifact: {
2282
+ ...artifact,
2283
+ ...base.artifact ?? {},
2284
+ recordingId: uploaded.recordingId,
2285
+ ...uploaded.jobId ? { jobId: uploaded.jobId } : {},
2286
+ ...uploaded.transcriptId ? { transcriptId: uploaded.transcriptId } : {},
2287
+ uploadStatus: "uploaded",
2288
+ uploadProgress: 1,
2289
+ transcriptionStatus,
2290
+ ...transcriptionStatus === "ready" ? { transcriptionProgress: 1 } : {}
2291
+ }
2292
+ };
2087
2293
  });
2088
2294
  setNotice(
2089
2295
  transcriptionStatus === "ready" ? "Transcription ready." : uploaded.jobId ? "Transcription queued." : "Uploaded to Recappi Cloud."
@@ -2127,13 +2333,13 @@ function AppShell({
2127
2333
  setLoadingMoreRecordings(false);
2128
2334
  }
2129
2335
  }, [fetchRecordings, loadingMoreRecordings, recordingsNextCursor]);
2130
- useEffect3(() => {
2336
+ useEffect4(() => {
2131
2337
  void refresh({ resetRecordings: true });
2132
2338
  const id = setInterval(() => void refresh(), pollMs);
2133
2339
  return () => clearInterval(id);
2134
2340
  }, [refresh, pollMs]);
2135
2341
  const hasRunning = jobs.some((item) => item.status === "running");
2136
- useEffect3(() => {
2342
+ useEffect4(() => {
2137
2343
  if (!hasRunning) return;
2138
2344
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
2139
2345
  return () => clearInterval(id);
@@ -2146,12 +2352,29 @@ function AppShell({
2146
2352
  jobStatusByRecording.set(job.recordingId, job.status);
2147
2353
  }
2148
2354
  }
2355
+ useEffect4(() => {
2356
+ setLiveRecord((current) => {
2357
+ if (current?.kind !== "stopped" || !current.artifact?.jobId) return current;
2358
+ const job = jobs.find((item) => item.jobId === current.artifact?.jobId);
2359
+ if (!job) return current;
2360
+ const fraction = transcribeFraction(job);
2361
+ return {
2362
+ ...current,
2363
+ artifact: {
2364
+ ...current.artifact,
2365
+ transcriptionStatus: transcriptionStatusFromJob(job.status),
2366
+ ...job.transcriptId ? { transcriptId: job.transcriptId } : {},
2367
+ ...job.status === "succeeded" ? { transcriptionProgress: 1 } : fraction != null ? { transcriptionProgress: fraction } : {}
2368
+ }
2369
+ };
2370
+ });
2371
+ }, [jobs]);
2149
2372
  const listLength = screen.kind === "jobs" ? jobs.length : screen.kind === "overview" ? recordings.length : 0;
2150
- useEffect3(() => {
2373
+ useEffect4(() => {
2151
2374
  setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
2152
2375
  }, [listLength]);
2153
2376
  const visibleRecordingRows = Math.max(3, size.rows - 6);
2154
- useEffect3(() => {
2377
+ useEffect4(() => {
2155
2378
  if (screen.kind !== "overview" || !recordingsNextCursor) return;
2156
2379
  const nearLoadedEnd = recordings.length - selected <= RECORDINGS_PREFETCH_REMAINING;
2157
2380
  const underfilledViewport = recordings.length < visibleRecordingRows;
@@ -2184,7 +2407,7 @@ function AppShell({
2184
2407
  [fetchTranscript]
2185
2408
  );
2186
2409
  const detailTranscriptId = screen.kind === "recordingDetail" ? recordings.find((r) => r.recordingId === screen.recordingId)?.activeTranscriptId : void 0;
2187
- useEffect3(() => {
2410
+ useEffect4(() => {
2188
2411
  if (!detailTranscriptId || transcriptCache.has(detailTranscriptId)) return;
2189
2412
  let cancelled = false;
2190
2413
  setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "loading"));
@@ -2344,8 +2567,10 @@ function AppShell({
2344
2567
  RecordSetupView,
2345
2568
  {
2346
2569
  model: recordSetupModel,
2570
+ levels: recordSetupLevels,
2347
2571
  onStart: beginLiveRecord,
2348
- onCancel: () => setStack((st) => st.length > 1 ? st.slice(0, -1) : [{ kind: "overview" }])
2572
+ onCancel: () => setStack((st) => st.length > 1 ? st.slice(0, -1) : [{ kind: "overview" }]),
2573
+ onSelectionChange: setRecordSetupSelection
2349
2574
  }
2350
2575
  ) });
2351
2576
  }
@@ -2371,7 +2596,7 @@ function AppShell({
2371
2596
  return /* @__PURE__ */ jsx18(PermissionPreflightView, { items: permissionItemsFromRecordError(liveRecord.data) });
2372
2597
  }
2373
2598
  const copy = recordErrorCopy(liveRecord.code, liveRecord.message);
2374
- return /* @__PURE__ */ jsxs15(Fragment5, { children: [
2599
+ return /* @__PURE__ */ jsxs15(Fragment6, { children: [
2375
2600
  /* @__PURE__ */ jsx18(Text16, { color: copy.tone, children: copy.title }),
2376
2601
  copy.detail ? /* @__PURE__ */ jsx18(Text16, { dimColor: true, children: copy.detail }) : null
2377
2602
  ] });
@@ -2524,6 +2749,7 @@ async function runDashboard(deps) {
2524
2749
  listDownloadedRecordingIds: deps.listDownloadedRecordingIds,
2525
2750
  fetchRecordSetup: deps.fetchRecordSetup,
2526
2751
  startLiveRecord: deps.startLiveRecord,
2752
+ startRecordSetupPreview: deps.startRecordSetupPreview,
2527
2753
  transcribeRecordingArtifact: deps.transcribeRecordingArtifact,
2528
2754
  initialView: deps.initialView ?? "overview",
2529
2755
  openUrl,
@@ -17284,6 +17510,19 @@ var sidecarRecordingStartResultSchema = external_exports.object({
17284
17510
  state: sidecarRecordingStateSchema,
17285
17511
  localSessionRef: external_exports.string().optional()
17286
17512
  });
17513
+ var sidecarLevelPreviewStartParamsSchema = external_exports.object({
17514
+ options: sidecarRecordingOptionsSchema
17515
+ });
17516
+ var sidecarLevelPreviewStartResultSchema = external_exports.object({
17517
+ previewId: external_exports.string()
17518
+ });
17519
+ var sidecarLevelPreviewStopParamsSchema = external_exports.object({
17520
+ previewId: external_exports.string()
17521
+ });
17522
+ var sidecarLevelPreviewStopResultSchema = external_exports.object({
17523
+ previewId: external_exports.string(),
17524
+ state: external_exports.literal("stopped")
17525
+ });
17287
17526
  var sidecarSessionParamsSchema = external_exports.object({
17288
17527
  sessionId: external_exports.string()
17289
17528
  });
@@ -17325,6 +17564,18 @@ var sidecarRequestSchema = external_exports.discriminatedUnion("method", [
17325
17564
  method: external_exports.literal("recappi.recording.start"),
17326
17565
  params: sidecarRecordingStartParamsSchema
17327
17566
  }),
17567
+ external_exports.object({
17568
+ jsonrpc: external_exports.literal("2.0"),
17569
+ id: sidecarJsonRpcIdSchema,
17570
+ method: external_exports.literal("recappi.recording.level_preview.start"),
17571
+ params: sidecarLevelPreviewStartParamsSchema
17572
+ }),
17573
+ external_exports.object({
17574
+ jsonrpc: external_exports.literal("2.0"),
17575
+ id: sidecarJsonRpcIdSchema,
17576
+ method: external_exports.literal("recappi.recording.level_preview.stop"),
17577
+ params: sidecarLevelPreviewStopParamsSchema
17578
+ }),
17328
17579
  external_exports.object({
17329
17580
  jsonrpc: external_exports.literal("2.0"),
17330
17581
  id: sidecarJsonRpcIdSchema,
@@ -17383,8 +17634,11 @@ var sidecarEventSchema = external_exports.discriminatedUnion("type", [
17383
17634
  }),
17384
17635
  external_exports.object({
17385
17636
  type: external_exports.literal("audio.level"),
17386
- sessionId: external_exports.string(),
17637
+ sessionId: external_exports.string().optional(),
17638
+ previewId: external_exports.string().optional(),
17387
17639
  input: external_exports.enum(["system", "microphone"]),
17640
+ sourceId: external_exports.string().optional(),
17641
+ microphoneDeviceId: external_exports.string().optional(),
17388
17642
  rmsDb: external_exports.number().optional(),
17389
17643
  peakDb: external_exports.number().optional(),
17390
17644
  at: external_exports.number().int().optional(),
@@ -20224,6 +20478,20 @@ var MiniSidecarClient = class {
20224
20478
  sidecarRecordingStartResultSchema
20225
20479
  );
20226
20480
  }
20481
+ startLevelPreview(params) {
20482
+ return this.request(
20483
+ "recappi.recording.level_preview.start",
20484
+ sidecarLevelPreviewStartParamsSchema.parse(params),
20485
+ sidecarLevelPreviewStartResultSchema
20486
+ );
20487
+ }
20488
+ stopLevelPreview(params) {
20489
+ return this.request(
20490
+ "recappi.recording.level_preview.stop",
20491
+ sidecarLevelPreviewStopParamsSchema.parse(params),
20492
+ sidecarLevelPreviewStopResultSchema
20493
+ );
20494
+ }
20227
20495
  getPermissionStatus(params) {
20228
20496
  return this.request(
20229
20497
  "recappi.permissions.status",
@@ -20573,6 +20841,55 @@ async function listRecordInputs(opts) {
20573
20841
  sidecar.kill();
20574
20842
  }
20575
20843
  }
20844
+ async function startRecordSetupLevelPreview(opts, selection = {
20845
+ sourceId: DEFAULT_RECORDING_SOURCES[0].id,
20846
+ includeMicrophone: true
20847
+ }, sources = DEFAULT_RECORDING_SOURCES) {
20848
+ const capture = recordingCaptureMappingFromSelection(selection, sources);
20849
+ const command = resolveSidecarCommand(opts);
20850
+ const sidecarArgs = opts.sidecarArgs ?? [];
20851
+ const spawnSidecar = opts.runtime?.spawnSidecar ?? spawnMiniSidecar;
20852
+ const sidecar = spawnSidecar({ command, args: sidecarArgs, env: opts.env });
20853
+ let previewId;
20854
+ let closed = false;
20855
+ const close = () => {
20856
+ if (closed) return;
20857
+ closed = true;
20858
+ sidecar.kill();
20859
+ };
20860
+ try {
20861
+ await sidecar.client.handshake(
20862
+ defaultSidecarHandshakeParams({
20863
+ client: { name: "recappi-cli", version: opts.cliVersion },
20864
+ capabilities: ["recording.capture"]
20865
+ })
20866
+ );
20867
+ const started = await sidecar.client.startLevelPreview({
20868
+ options: {
20869
+ includeSystemAudio: capture.includeSystemAudio,
20870
+ includeMicrophone: capture.includeMicrophone,
20871
+ liveCaptions: false,
20872
+ ...capture.targetBundleId ? { targetBundleId: capture.targetBundleId } : {},
20873
+ ...capture.microphoneDeviceId ? { microphoneDeviceId: capture.microphoneDeviceId } : {}
20874
+ }
20875
+ });
20876
+ previewId = started.previewId;
20877
+ return {
20878
+ source: sidecar.client,
20879
+ stop: async () => {
20880
+ try {
20881
+ if (previewId) await sidecar.client.stopLevelPreview({ previewId });
20882
+ } catch {
20883
+ } finally {
20884
+ close();
20885
+ }
20886
+ }
20887
+ };
20888
+ } catch (error51) {
20889
+ close();
20890
+ throw error51;
20891
+ }
20892
+ }
20576
20893
  async function startRecordSession(opts) {
20577
20894
  let retriedAfterMicrophoneGrant = false;
20578
20895
  while (true) {
@@ -21120,14 +21437,24 @@ async function runCli(deps = {}) {
21120
21437
  sources
21121
21438
  );
21122
21439
  },
21123
- transcribeRecordingArtifact: async (artifact) => {
21440
+ startRecordSetupPreview: async (selection, sources) => startRecordSetupLevelPreview(
21441
+ {
21442
+ cliVersion: CLI_VERSION,
21443
+ env: deps.env,
21444
+ runtime: deps.recordRuntime
21445
+ },
21446
+ selection,
21447
+ sources
21448
+ ),
21449
+ transcribeRecordingArtifact: async (artifact, onEvent) => {
21124
21450
  if (!artifact.audioPath) {
21125
21451
  throw cliError("input.not_found", "No local audio file is available to transcribe.");
21126
21452
  }
21127
21453
  const data = await client.uploadPathBatch({
21128
21454
  inputPath: artifact.audioPath,
21129
21455
  transcribe: true,
21130
- wait: false
21456
+ wait: false,
21457
+ onEvent
21131
21458
  });
21132
21459
  if (data.failures.length > 0) {
21133
21460
  const failure = data.failures[0];