recappi 0.1.4 → 0.1.5

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
@@ -268,17 +268,317 @@ var init_terminal = __esm({
268
268
  }
269
269
  });
270
270
 
271
- // src/tui/chrome.tsx
271
+ // src/tui/liveCaptions.ts
272
+ function initialLiveCaptionsState() {
273
+ return { status: "connecting", lines: [] };
274
+ }
275
+ function liveCaptionReducer(state, event) {
276
+ switch (event.kind) {
277
+ case "status": {
278
+ const next = { ...state, status: event.status };
279
+ if (event.status === "live" && state.startedAtMs == null) {
280
+ next.startedAtMs = event.atMs ?? state.startedAtMs;
281
+ }
282
+ if (event.status !== "error") next.error = void 0;
283
+ return next;
284
+ }
285
+ case "partial":
286
+ return { ...state, partial: event.text };
287
+ case "final": {
288
+ const lines = [...state.lines, event.line];
289
+ return { ...state, lines, partial: void 0 };
290
+ }
291
+ case "translationPartial":
292
+ return { ...state, translationPartial: event.text };
293
+ case "translationFinal": {
294
+ const lines = [...state.lines];
295
+ let idx = event.segmentId ? lines.findIndex((l) => l.id === event.segmentId) : -1;
296
+ if (idx < 0) idx = lines.length - 1;
297
+ if (idx >= 0) lines[idx] = { ...lines[idx], translation: event.text };
298
+ return { ...state, lines, translationPartial: void 0 };
299
+ }
300
+ case "error":
301
+ return { ...state, status: "error", error: event.message };
302
+ default:
303
+ return state;
304
+ }
305
+ }
306
+ function recordingStateToStatus(state) {
307
+ switch (state) {
308
+ case "idle":
309
+ case "starting":
310
+ return "connecting";
311
+ case "recording":
312
+ return "live";
313
+ case "stopping":
314
+ case "finalizing":
315
+ case "uploading":
316
+ case "completed":
317
+ case "cancelled":
318
+ return "stopped";
319
+ case "failed":
320
+ return "error";
321
+ }
322
+ }
323
+ function sidecarToLiveCaptionEvent(event) {
324
+ switch (event.type) {
325
+ case "ready":
326
+ return { kind: "status", status: "connecting" };
327
+ case "recording.state":
328
+ if (event.state === "failed") {
329
+ return { kind: "error", message: event.message ?? "Recording failed" };
330
+ }
331
+ return { kind: "status", status: recordingStateToStatus(event.state) };
332
+ case "live_caption.delta": {
333
+ if (event.stream === "translation") {
334
+ return event.isFinal ? { kind: "translationFinal", segmentId: event.segmentId, text: event.text } : { kind: "translationPartial", text: event.text };
335
+ }
336
+ if (event.isFinal) {
337
+ return {
338
+ kind: "final",
339
+ line: {
340
+ id: event.segmentId ?? `${event.startMs ?? event.atMs ?? 0}`,
341
+ text: event.text,
342
+ speaker: event.speaker,
343
+ atMs: event.startMs ?? event.atMs
344
+ }
345
+ };
346
+ }
347
+ return { kind: "partial", text: event.text };
348
+ }
349
+ case "error":
350
+ return { kind: "error", message: event.message };
351
+ case "audio.level":
352
+ case "local_artifact.upserted":
353
+ return null;
354
+ default:
355
+ return null;
356
+ }
357
+ }
358
+ function liveCaptionStatusLabel(status) {
359
+ switch (status) {
360
+ case "connecting":
361
+ return "Connecting\u2026";
362
+ case "live":
363
+ return "\u25CF LIVE";
364
+ case "reconnecting":
365
+ return "Reconnecting\u2026";
366
+ case "stopped":
367
+ return "Stopped";
368
+ case "error":
369
+ return "Error";
370
+ }
371
+ }
372
+ var init_liveCaptions = __esm({
373
+ "src/tui/liveCaptions.ts"() {
374
+ "use strict";
375
+ }
376
+ });
377
+
378
+ // src/tui/LiveCaptionsView.tsx
379
+ import { useMemo, useState } from "react";
380
+ import { Box, Text, useInput } from "ink";
381
+ import { jsx, jsxs } from "react/jsx-runtime";
382
+ function LiveCaptionsView({
383
+ state,
384
+ nowMs
385
+ }) {
386
+ const size = useTerminalSize();
387
+ const [scrollUp, setScrollUp] = useState(0);
388
+ const innerWidth = Math.max(10, size.columns - 2);
389
+ const items = useMemo(() => {
390
+ const rows = [];
391
+ for (const l of state.lines) {
392
+ rows.push({ key: l.id, kind: "final", speaker: l.speaker, text: l.text });
393
+ if (l.translation) rows.push({ key: `${l.id}__t`, kind: "translation", text: l.translation });
394
+ }
395
+ if (state.partial && state.partial.length > 0) {
396
+ rows.push({ key: "__partial__", kind: "partial", text: state.partial });
397
+ }
398
+ if (state.translationPartial && state.translationPartial.length > 0) {
399
+ rows.push({ key: "__tpartial__", kind: "translation", text: state.translationPartial });
400
+ }
401
+ return rows;
402
+ }, [state.lines, state.partial, state.translationPartial]);
403
+ const heights = useMemo(
404
+ () => items.map((it) => {
405
+ const prefix = it.kind === "translation" ? "\u21B3 " : it.speaker ? `${it.speaker}: ` : "";
406
+ return Math.max(1, Math.ceil(displayWidth(prefix + it.text) / innerWidth));
407
+ }),
408
+ [items, innerWidth]
409
+ );
410
+ const budget = Math.max(3, size.rows - 3);
411
+ const maxScroll = windowByHeights(heights, Number.MAX_SAFE_INTEGER, budget).maxScroll;
412
+ const top = Math.max(0, maxScroll - scrollUp);
413
+ const win = windowByHeights(heights, top, budget);
414
+ const following = scrollUp === 0;
415
+ const page = Math.max(1, budget - 1);
416
+ useInput((input, key) => {
417
+ if (key.upArrow || input === "k") setScrollUp((s) => Math.min(maxScroll, s + 1));
418
+ else if (key.downArrow || input === "j") setScrollUp((s) => Math.max(0, s - 1));
419
+ else if (key.pageUp || input === "b") setScrollUp((s) => Math.min(maxScroll, s + page));
420
+ else if (key.pageDown || input === " ") setScrollUp((s) => Math.max(0, s - page));
421
+ else if (input === "G") setScrollUp(0);
422
+ else if (input === "g") setScrollUp(maxScroll);
423
+ });
424
+ const elapsed = state.startedAtMs != null ? formatClockMs(Math.max(0, nowMs - state.startedAtMs)) : null;
425
+ const statusColor = STATUS_COLOR[state.status] ?? "white";
426
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
427
+ /* @__PURE__ */ jsxs(Text, { children: [
428
+ /* @__PURE__ */ jsx(Text, { bold: true, color: statusColor, children: liveCaptionStatusLabel(state.status) }),
429
+ elapsed ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${elapsed}` }) : null,
430
+ !following ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u23F8 scrolled \u2014 G for live" }) : null
431
+ ] }),
432
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: items.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: state.status === "error" ? state.error ? `Error: ${state.error}` : "Live captions error" : "Waiting for captions\u2026" }) : items.slice(win.start, win.end).map(
433
+ (it) => it.kind === "translation" ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u21B3 ${it.text}` }, it.key) : /* @__PURE__ */ jsxs(Text, { dimColor: it.kind === "partial", italic: it.kind === "partial", children: [
434
+ it.speaker ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: `${it.speaker}: ` }) : null,
435
+ it.text
436
+ ] }, it.key)
437
+ ) }),
438
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
439
+ maxScroll > 0 ? "\u2191\u2193 scroll \xB7 G live \xB7 " : "",
440
+ "q / esc / \u2190 back"
441
+ ] }) })
442
+ ] });
443
+ }
444
+ var STATUS_COLOR;
445
+ var init_LiveCaptionsView = __esm({
446
+ "src/tui/LiveCaptionsView.tsx"() {
447
+ "use strict";
448
+ init_format();
449
+ init_terminal();
450
+ init_liveCaptions();
451
+ STATUS_COLOR = {
452
+ connecting: "yellow",
453
+ live: "red",
454
+ reconnecting: "yellow",
455
+ stopped: "gray",
456
+ error: "red"
457
+ };
458
+ }
459
+ });
460
+
461
+ // src/tui/LiveCaptionsScreen.tsx
462
+ import { useEffect, useState as useState2 } from "react";
463
+ import { jsx as jsx2 } from "react/jsx-runtime";
464
+ function LiveCaptionsScreen({
465
+ source,
466
+ now = () => Date.now()
467
+ }) {
468
+ const [state, setState] = useState2(initialLiveCaptionsState);
469
+ const [tick, setTick] = useState2(() => now());
470
+ useEffect(() => {
471
+ const unsubscribe = source.onEvent((event) => {
472
+ const mapped = sidecarToLiveCaptionEvent(event);
473
+ if (mapped) setState((s) => liveCaptionReducer(s, mapped));
474
+ });
475
+ return unsubscribe;
476
+ }, [source]);
477
+ useEffect(() => {
478
+ const id = setInterval(() => setTick(now()), 1e3);
479
+ return () => clearInterval(id);
480
+ }, []);
481
+ return /* @__PURE__ */ jsx2(LiveCaptionsView, { state, nowMs: tick });
482
+ }
483
+ var init_LiveCaptionsScreen = __esm({
484
+ "src/tui/LiveCaptionsScreen.tsx"() {
485
+ "use strict";
486
+ init_LiveCaptionsView();
487
+ init_liveCaptions();
488
+ }
489
+ });
490
+
491
+ // src/tui/AccountView.tsx
272
492
  import { Box as Box2, Text as Text2 } from "ink";
273
- import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
493
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
494
+ function AccountView({ status }) {
495
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
496
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "\u2039 Account" }),
497
+ status === "loading" || status === void 0 ? /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Loading account\u2026" }) }) : status === "error" ? /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { color: "red", children: "Couldn't load account status" }) }) : !status.loggedIn ? /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
498
+ /* @__PURE__ */ jsx4(Text2, { color: "yellow", children: "Not signed in" }),
499
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: `origin ${status.origin}` }),
500
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Run `recappi auth login` to sign in." })
501
+ ] }) : /* @__PURE__ */ jsx4(AccountBody, { status }),
502
+ /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "r refresh \xB7 esc back \xB7 q quit" }) })
503
+ ] });
504
+ }
505
+ function AccountBody({ status }) {
506
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
507
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
508
+ /* @__PURE__ */ jsx4(Text2, { bold: true, color: "magenta", children: status.email ?? status.userId ?? "Signed in" }),
509
+ status.email && status.userId ? /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: status.userId }) : null,
510
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: `origin ${status.origin}` })
511
+ ] }),
512
+ status.billing ? /* @__PURE__ */ jsx4(Usage, { billing: status.billing }) : null,
513
+ /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
514
+ /* @__PURE__ */ jsx4(Text2, { bold: true, children: "Local store" }),
515
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, wrap: "truncate-middle", children: status.localStore.path }),
516
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
517
+ `${status.localStore.accountScopedArtifacts} artifact${status.localStore.accountScopedArtifacts === 1 ? "" : "s"} for this account`,
518
+ status.localStore.unattributedArtifacts > 0 ? ` \xB7 ${status.localStore.unattributedArtifacts} unattributed` : ""
519
+ ] })
520
+ ] })
521
+ ] });
522
+ }
523
+ function Usage({ billing }) {
524
+ const minutesCap = billing.minutesCap;
525
+ const minutesUsed = billing.minutesUsed;
526
+ const storageCap = billing.storageCapBytes;
527
+ return /* @__PURE__ */ jsxs2(Box2, { marginTop: 1, flexDirection: "column", children: [
528
+ /* @__PURE__ */ jsxs2(Text2, { children: [
529
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Plan " }),
530
+ /* @__PURE__ */ jsx4(Text2, { bold: true, children: billing.tier })
531
+ ] }),
532
+ /* @__PURE__ */ jsxs2(Text2, { children: [
533
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Minutes " }),
534
+ minutesCap != null ? /* @__PURE__ */ jsx4(Text2, { color: billing.isOverMinutes ? "red" : "cyan", children: `${progressBar(minutesUsed / Math.max(1, minutesCap), 12)} ` }) : null,
535
+ /* @__PURE__ */ jsx4(Text2, { color: billing.isOverMinutes ? "red" : void 0, children: `${Math.round(minutesUsed)}` }),
536
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: ` / ${minutesCap != null ? Math.round(minutesCap) : "\u221E"} min` }),
537
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: ` (batch ${Math.round(billing.batchMinutesUsed)} \xB7 live ${Math.round(billing.realtimeMinutesUsed)})` })
538
+ ] }),
539
+ /* @__PURE__ */ jsxs2(Text2, { children: [
540
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: "Storage " }),
541
+ storageCap != null ? /* @__PURE__ */ jsx4(Text2, { color: billing.isOverStorage ? "red" : "cyan", children: `${progressBar(billing.storageBytes / Math.max(1, storageCap), 12)} ` }) : null,
542
+ /* @__PURE__ */ jsx4(Text2, { color: billing.isOverStorage ? "red" : void 0, children: formatBytes2(billing.storageBytes) }),
543
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: ` / ${storageCap != null ? formatBytes2(storageCap) : "\u221E"}` })
544
+ ] }),
545
+ billing.isOverMinutes || billing.isOverStorage ? /* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
546
+ billing.isOverMinutes ? "Over minutes limit. " : "",
547
+ billing.isOverStorage ? "Over storage limit." : ""
548
+ ] }) : null,
549
+ /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: `Period ${periodText(billing)}` })
550
+ ] });
551
+ }
552
+ function periodText(billing) {
553
+ const remainingMs = epochToMs(billing.periodEnd) - Date.now();
554
+ if (!Number.isFinite(remainingMs) || remainingMs <= 0) return "\u2014";
555
+ const days = Math.floor(remainingMs / 864e5);
556
+ if (days >= 1) return `${days}d left`;
557
+ return `${formatClockMs(remainingMs)} left`;
558
+ }
559
+ function epochToMs(value) {
560
+ return value > 1e12 ? value : value * 1e3;
561
+ }
562
+ var init_AccountView = __esm({
563
+ "src/tui/AccountView.tsx"() {
564
+ "use strict";
565
+ init_format();
566
+ }
567
+ });
568
+
569
+ // src/tui/chrome.tsx
570
+ import { Box as Box3, Text as Text3 } from "ink";
571
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
274
572
  function Header({ active }) {
275
- return /* @__PURE__ */ jsxs2(Box2, { children: [
276
- /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "magenta", children: [
573
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
574
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "magenta", children: [
277
575
  "Recappi",
278
576
  " "
279
577
  ] }),
280
- /* @__PURE__ */ jsx4(Tab, { num: "1", label: "Overview", active: active === "overview" }),
281
- /* @__PURE__ */ jsx4(Tab, { num: "2", label: "Jobs", active: active === "jobs" })
578
+ /* @__PURE__ */ jsx5(Tab, { num: "1", label: "Overview", active: active === "overview" }),
579
+ /* @__PURE__ */ jsx5(Tab, { num: "2", label: "Jobs", active: active === "jobs" }),
580
+ /* @__PURE__ */ jsx5(Tab, { num: "3", label: "Account", active: active === "account" }),
581
+ /* @__PURE__ */ jsx5(Tab, { num: "4", label: "Record", active: active === "record" })
282
582
  ] });
283
583
  }
284
584
  function Tab({
@@ -288,12 +588,12 @@ function Tab({
288
588
  }) {
289
589
  const text = ` ${num} ${label} `;
290
590
  if (active) {
291
- return /* @__PURE__ */ jsx4(Text2, { bold: true, inverse: true, color: "cyan", children: text });
591
+ return /* @__PURE__ */ jsx5(Text3, { bold: true, inverse: true, color: "cyan", children: text });
292
592
  }
293
- return /* @__PURE__ */ jsx4(Text2, { children: text });
593
+ return /* @__PURE__ */ jsx5(Text3, { children: text });
294
594
  }
295
595
  function Footer({ keys }) {
296
- return /* @__PURE__ */ jsx4(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text2, { dimColor: true, children: keys }) });
596
+ return /* @__PURE__ */ jsx5(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: keys }) });
297
597
  }
298
598
  var init_chrome = __esm({
299
599
  "src/tui/chrome.tsx"() {
@@ -302,8 +602,8 @@ var init_chrome = __esm({
302
602
  });
303
603
 
304
604
  // src/tui/JobRow.tsx
305
- import { Box as Box3, Text as Text3 } from "ink";
306
- import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
605
+ import { Box as Box4, Text as Text4 } from "ink";
606
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
307
607
  function JobRow({
308
608
  item,
309
609
  selected,
@@ -312,11 +612,11 @@ function JobRow({
312
612
  const style = statusStyle(item.status);
313
613
  const glyph = statusGlyph(item.status, spinnerFrame);
314
614
  const title = item.recording?.title ?? item.recordingId;
315
- return /* @__PURE__ */ jsxs3(Box3, { children: [
316
- /* @__PURE__ */ jsx5(Text3, { color: "cyan", children: selected ? "\u25B8 " : " " }),
317
- /* @__PURE__ */ jsx5(Text3, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
318
- /* @__PURE__ */ jsx5(Text3, { bold: selected, children: padCell(title, 24) }),
319
- /* @__PURE__ */ jsx5(Text3, { dimColor: !selected, children: jobDetail(item) })
615
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
616
+ /* @__PURE__ */ jsx6(Text4, { color: "cyan", children: selected ? "\u25B8 " : " " }),
617
+ /* @__PURE__ */ jsx6(Text4, { color: style.color, children: `${glyph} ${padCell(style.label, 13)}` }),
618
+ /* @__PURE__ */ jsx6(Text4, { bold: selected, children: padCell(title, 24) }),
619
+ /* @__PURE__ */ jsx6(Text4, { dimColor: !selected, children: jobDetail(item) })
320
620
  ] });
321
621
  }
322
622
  var init_JobRow = __esm({
@@ -327,17 +627,17 @@ var init_JobRow = __esm({
327
627
  });
328
628
 
329
629
  // src/tui/JobsView.tsx
330
- import { Box as Box4, Text as Text4 } from "ink";
331
- import { jsx as jsx6 } from "react/jsx-runtime";
630
+ import { Box as Box5, Text as Text5 } from "ink";
631
+ import { jsx as jsx7 } from "react/jsx-runtime";
332
632
  function JobsView({
333
633
  items,
334
634
  selectedIndex,
335
635
  spinnerFrame
336
636
  }) {
337
637
  if (items.length === 0) {
338
- return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text4, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
638
+ return /* @__PURE__ */ jsx7(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: "No transcription jobs yet \u2014 run: recappi upload <file> --transcribe" }) });
339
639
  }
340
- return /* @__PURE__ */ jsx6(Box4, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx6(
640
+ return /* @__PURE__ */ jsx7(Box5, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx7(
341
641
  JobRow,
342
642
  {
343
643
  item,
@@ -355,8 +655,8 @@ var init_JobsView = __esm({
355
655
  });
356
656
 
357
657
  // src/tui/RecordingRow.tsx
358
- import { Box as Box5, Text as Text5 } from "ink";
359
- import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
658
+ import { Box as Box6, Text as Text6 } from "ink";
659
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
360
660
  function recordingTitle2(item) {
361
661
  const named = (item.title || item.summaryTitle || "").trim();
362
662
  if (named && !UUID_RE.test(named)) return named;
@@ -392,22 +692,22 @@ function RecordingRow({
392
692
  const { title, showWhen } = recordingLayout(columns);
393
693
  const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
394
694
  const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
395
- return /* @__PURE__ */ jsxs4(Box5, { children: [
396
- /* @__PURE__ */ jsx7(Text5, { color: "cyan", children: selected ? "\u25B8 " : " " }),
397
- /* @__PURE__ */ jsx7(Text5, { color, children: `${glyph} ` }),
398
- /* @__PURE__ */ jsx7(Text5, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
399
- /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
400
- showWhen ? /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay(formatAge(item.createdAt, nowMs), WHEN_W) }) : null,
401
- /* @__PURE__ */ jsx7(Text5, { color: "green", children: downloaded ? " \u2913" : " " })
695
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
696
+ /* @__PURE__ */ jsx8(Text6, { color: "cyan", children: selected ? "\u25B8 " : " " }),
697
+ /* @__PURE__ */ jsx8(Text6, { color, children: `${glyph} ` }),
698
+ /* @__PURE__ */ jsx8(Text6, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
699
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
700
+ showWhen ? /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay(formatAge(item.createdAt, nowMs), WHEN_W) }) : null,
701
+ /* @__PURE__ */ jsx8(Text6, { color: "green", children: downloaded ? " \u2913" : " " })
402
702
  ] });
403
703
  }
404
704
  function RecordingHeader({ columns }) {
405
705
  const { title, showWhen } = recordingLayout(columns);
406
- return /* @__PURE__ */ jsxs4(Box5, { children: [
407
- /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
408
- /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("TITLE", title) }),
409
- /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
410
- showWhen ? /* @__PURE__ */ jsx7(Text5, { dimColor: true, children: "WHEN" }) : null
706
+ return /* @__PURE__ */ jsxs5(Box6, { children: [
707
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
708
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay("TITLE", title) }),
709
+ /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
710
+ showWhen ? /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "WHEN" }) : null
411
711
  ] });
412
712
  }
413
713
  var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W;
@@ -425,8 +725,8 @@ var init_RecordingRow = __esm({
425
725
 
426
726
  // src/tui/RecordingsView.tsx
427
727
  import React3 from "react";
428
- import { Box as Box6, Text as Text6 } from "ink";
429
- import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
728
+ import { Box as Box7, Text as Text7 } from "ink";
729
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
430
730
  function RecordingsView({
431
731
  items,
432
732
  selectedIndex,
@@ -437,16 +737,16 @@ function RecordingsView({
437
737
  spinnerFrame = 0
438
738
  }) {
439
739
  if (items.length === 0) {
440
- return /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text6, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
740
+ return /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
441
741
  }
442
- return /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
443
- /* @__PURE__ */ jsx8(RecordingHeader, { columns }),
742
+ return /* @__PURE__ */ jsxs6(Box7, { marginTop: 1, flexDirection: "column", children: [
743
+ /* @__PURE__ */ jsx9(RecordingHeader, { columns }),
444
744
  items.map((item, index) => {
445
745
  const bucket = dateBucket(item.createdAt, nowMs);
446
746
  const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
447
- return /* @__PURE__ */ jsxs5(React3.Fragment, { children: [
448
- showHeader ? /* @__PURE__ */ jsx8(Box6, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx8(Text6, { bold: true, color: "blue", children: bucket }) }) : null,
449
- /* @__PURE__ */ jsx8(
747
+ return /* @__PURE__ */ jsxs6(React3.Fragment, { children: [
748
+ showHeader ? /* @__PURE__ */ jsx9(Box7, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx9(Text7, { bold: true, color: "blue", children: bucket }) }) : null,
749
+ /* @__PURE__ */ jsx9(
450
750
  RecordingRow,
451
751
  {
452
752
  item,
@@ -471,15 +771,15 @@ var init_RecordingsView = __esm({
471
771
  });
472
772
 
473
773
  // src/tui/RecordingPeek.tsx
474
- import { Box as Box7, Text as Text7 } from "ink";
475
- import { Fragment, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
774
+ import { Box as Box8, Text as Text8 } from "ink";
775
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
476
776
  function RecordingPeek({
477
777
  item,
478
778
  summary,
479
779
  nowMs,
480
780
  width
481
781
  }) {
482
- return /* @__PURE__ */ jsx9(Box7, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx9(PeekBody, { item, summary, nowMs }) });
782
+ return /* @__PURE__ */ jsx10(Box8, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx10(PeekBody, { item, summary, nowMs }) });
483
783
  }
484
784
  function PeekBody({
485
785
  item,
@@ -492,30 +792,30 @@ function PeekBody({
492
792
  formatBytes2(item.sizeBytes) || null,
493
793
  item.contentType || null
494
794
  ].filter(Boolean).join(" \xB7 ");
495
- return /* @__PURE__ */ jsxs6(Fragment, { children: [
496
- /* @__PURE__ */ jsx9(Text7, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
497
- /* @__PURE__ */ jsx9(Text7, { color: style.color, children: `${style.glyph} ${style.label}` }),
498
- meta3 ? /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: meta3 }) : null,
499
- /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
500
- /* @__PURE__ */ jsx9(Box7, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx9(SummarySection, { item, summary }) }),
501
- /* @__PURE__ */ jsx9(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
795
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
796
+ /* @__PURE__ */ jsx10(Text8, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
797
+ /* @__PURE__ */ jsx10(Text8, { color: style.color, children: `${style.glyph} ${style.label}` }),
798
+ meta3 ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: meta3 }) : null,
799
+ /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
800
+ /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(SummarySection, { item, summary }) }),
801
+ /* @__PURE__ */ jsx10(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
502
802
  ] });
503
803
  }
504
804
  function SummarySection({
505
805
  item,
506
806
  summary
507
807
  }) {
508
- if (!item.activeTranscriptId) return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "No transcript yet" });
509
- if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "Loading summary\u2026" });
510
- if (summary === "error") return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: "(summary unavailable)" });
808
+ if (!item.activeTranscriptId) return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "No transcript yet" });
809
+ if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Loading summary\u2026" });
810
+ if (summary === "error") return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "(summary unavailable)" });
511
811
  if (summary.status !== "succeeded" || !summary.tldr) {
512
- return /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: `Summary ${summary.status}` });
812
+ return /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: `Summary ${summary.status}` });
513
813
  }
514
814
  const points = (summary.keyPoints ?? []).slice(0, 3);
515
- return /* @__PURE__ */ jsxs6(Fragment, { children: [
516
- /* @__PURE__ */ jsx9(Text7, { bold: true, children: "Summary" }),
517
- /* @__PURE__ */ jsx9(Text7, { children: summary.tldr }),
518
- points.length > 0 ? /* @__PURE__ */ jsx9(Box7, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx9(Text7, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
815
+ return /* @__PURE__ */ jsxs7(Fragment2, { children: [
816
+ /* @__PURE__ */ jsx10(Text8, { bold: true, children: "Summary" }),
817
+ /* @__PURE__ */ jsx10(Text8, { children: summary.tldr }),
818
+ points.length > 0 ? /* @__PURE__ */ jsx10(Box8, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx10(Text8, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
519
819
  ] });
520
820
  }
521
821
  var init_RecordingPeek = __esm({
@@ -527,8 +827,8 @@ var init_RecordingPeek = __esm({
527
827
  });
528
828
 
529
829
  // src/tui/OverviewView.tsx
530
- import { Box as Box8, Text as Text8 } from "ink";
531
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
830
+ import { Box as Box9, Text as Text9 } from "ink";
831
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
532
832
  function OverviewView({
533
833
  recordings,
534
834
  jobs,
@@ -547,17 +847,17 @@ function OverviewView({
547
847
  const jobCounts = countJobs(jobs);
548
848
  const running = stats?.jobs.running ?? jobCounts.running;
549
849
  const queued = stats?.jobs.queued ?? jobCounts.queued;
550
- return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", children: [
551
- /* @__PURE__ */ jsxs7(Box8, { children: [
552
- /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: "Recordings " }),
553
- /* @__PURE__ */ jsx10(Text8, { bold: true, children: stats?.recordings.total ?? recordings.length }),
554
- stats?.recordings.ready != null ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
555
- stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx10(Text8, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
556
- running > 0 ? /* @__PURE__ */ jsx10(Text8, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
557
- queued > 0 ? /* @__PURE__ */ jsx10(Text8, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
850
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", children: [
851
+ /* @__PURE__ */ jsxs8(Box9, { children: [
852
+ /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Recordings " }),
853
+ /* @__PURE__ */ jsx11(Text9, { bold: true, children: stats?.recordings.total ?? recordings.length }),
854
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
855
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
856
+ running > 0 ? /* @__PURE__ */ jsx11(Text9, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
857
+ queued > 0 ? /* @__PURE__ */ jsx11(Text9, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
558
858
  ] }),
559
- /* @__PURE__ */ jsxs7(Box8, { flexDirection: "row", alignItems: "flex-start", children: [
560
- /* @__PURE__ */ jsx10(Box8, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx10(
859
+ /* @__PURE__ */ jsxs8(Box9, { flexDirection: "row", alignItems: "flex-start", children: [
860
+ /* @__PURE__ */ jsx11(Box9, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(
561
861
  RecordingsView,
562
862
  {
563
863
  items: recordings,
@@ -569,7 +869,7 @@ function OverviewView({
569
869
  spinnerFrame
570
870
  }
571
871
  ) }),
572
- showPeek ? /* @__PURE__ */ jsx10(Box8, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx10(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
872
+ showPeek ? /* @__PURE__ */ jsx11(Box9, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx11(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
573
873
  ] })
574
874
  ] });
575
875
  }
@@ -583,8 +883,8 @@ var init_OverviewView = __esm({
583
883
  });
584
884
 
585
885
  // src/tui/JobDetailView.tsx
586
- import { Box as Box9, Text as Text9 } from "ink";
587
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
886
+ import { Box as Box10, Text as Text10 } from "ink";
887
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
588
888
  function JobDetailView({
589
889
  item,
590
890
  origin,
@@ -594,13 +894,13 @@ function JobDetailView({
594
894
  const style = statusStyle(item.status);
595
895
  const links = resolveJobLinks(item, origin);
596
896
  const title = item.recording?.title ?? item.recordingId;
597
- return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 1, children: [
598
- /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
897
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
898
+ /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
599
899
  "\u2039 Jobs / ",
600
900
  title
601
901
  ] }),
602
- /* @__PURE__ */ jsxs8(
603
- Box9,
902
+ /* @__PURE__ */ jsxs9(
903
+ Box10,
604
904
  {
605
905
  marginTop: 1,
606
906
  borderStyle: "round",
@@ -608,17 +908,17 @@ function JobDetailView({
608
908
  paddingX: 1,
609
909
  flexDirection: "column",
610
910
  children: [
611
- /* @__PURE__ */ jsxs8(Text9, { color: style.color, bold: true, children: [
911
+ /* @__PURE__ */ jsxs9(Text10, { color: style.color, bold: true, children: [
612
912
  style.label,
613
- item.provider ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${item.provider}` }) : null
913
+ item.provider ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${item.provider}` }) : null
614
914
  ] }),
615
- /* @__PURE__ */ jsx11(StatusLine, { item, spinnerFrame, nowMs })
915
+ /* @__PURE__ */ jsx12(StatusLine, { item, spinnerFrame, nowMs })
616
916
  ]
617
917
  }
618
918
  ),
619
- /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
620
- /* @__PURE__ */ jsx11(Text9, { bold: true, children: "Timeline" }),
621
- /* @__PURE__ */ jsx11(
919
+ /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
920
+ /* @__PURE__ */ jsx12(Text10, { bold: true, children: "Timeline" }),
921
+ /* @__PURE__ */ jsx12(
622
922
  TimelineRow,
623
923
  {
624
924
  label: "Enqueued",
@@ -627,7 +927,7 @@ function JobDetailView({
627
927
  nowMs
628
928
  }
629
929
  ),
630
- /* @__PURE__ */ jsx11(
930
+ /* @__PURE__ */ jsx12(
631
931
  TimelineRow,
632
932
  {
633
933
  label: "Started",
@@ -636,7 +936,7 @@ function JobDetailView({
636
936
  nowMs
637
937
  }
638
938
  ),
639
- /* @__PURE__ */ jsx11(
939
+ /* @__PURE__ */ jsx12(
640
940
  TimelineRow,
641
941
  {
642
942
  label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
@@ -648,21 +948,21 @@ function JobDetailView({
648
948
  }
649
949
  )
650
950
  ] }),
651
- /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
652
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "Recording " }),
951
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
952
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Recording " }),
653
953
  title,
654
- item.recording?.durationMs ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
954
+ item.recording?.durationMs ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
655
955
  ] }) }),
656
- /* @__PURE__ */ jsx11(Box9, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs8(Text9, { children: [
657
- /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
658
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
659
- /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
660
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
661
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "m mac app (soon)" }),
662
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: " \xB7 " }),
663
- /* @__PURE__ */ jsx11(Text9, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
956
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs9(Text10, { children: [
957
+ /* @__PURE__ */ jsx12(Text10, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
958
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
959
+ /* @__PURE__ */ jsx12(Text10, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
960
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
961
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "m mac app (soon)" }),
962
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " \xB7 " }),
963
+ /* @__PURE__ */ jsx12(Text10, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
664
964
  ] }) }),
665
- /* @__PURE__ */ jsx11(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
965
+ /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
666
966
  "esc back \xB7 t transcript",
667
967
  item.transcriptId ? "" : " (when ready)",
668
968
  " \xB7 q quit"
@@ -679,24 +979,24 @@ function StatusLine({
679
979
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
680
980
  if (fraction != null) {
681
981
  const pct = Math.round(fraction * 100);
682
- return /* @__PURE__ */ jsxs8(Text9, { children: [
982
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
683
983
  `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
684
984
  item.recording?.durationMs
685
985
  )}`,
686
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: elapsed })
986
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: elapsed })
687
987
  ] });
688
988
  }
689
989
  const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
690
- return /* @__PURE__ */ jsxs8(Text9, { children: [
990
+ return /* @__PURE__ */ jsxs9(Text10, { children: [
691
991
  `${spinner} transcribing\u2026`,
692
- /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: elapsed })
992
+ /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: elapsed })
693
993
  ] });
694
994
  }
695
995
  if (item.status === "succeeded")
696
- return /* @__PURE__ */ jsx11(Text9, { children: item.transcriptId ? "transcript ready" : "done" });
697
- if (item.status === "queued") return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: "waiting to start\u2026" });
698
- if (item.status === "failed") return /* @__PURE__ */ jsx11(Text9, { color: "red", children: "transcription failed" });
699
- return /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: item.status });
996
+ return /* @__PURE__ */ jsx12(Text10, { children: item.transcriptId ? "transcript ready" : "done" });
997
+ if (item.status === "queued") return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "waiting to start\u2026" });
998
+ if (item.status === "failed") return /* @__PURE__ */ jsx12(Text10, { color: "red", children: "transcription failed" });
999
+ return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: item.status });
700
1000
  }
701
1001
  function TimelineRow({
702
1002
  label,
@@ -709,10 +1009,10 @@ function TimelineRow({
709
1009
  const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
710
1010
  const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
711
1011
  const age = at ? formatAge(at, nowMs) : running ? "now" : "";
712
- return /* @__PURE__ */ jsxs8(Box9, { children: [
713
- /* @__PURE__ */ jsx11(Text9, { color, children: ` ${glyph} ` }),
714
- /* @__PURE__ */ jsx11(Text9, { dimColor: !done && !running, children: label }),
715
- age ? /* @__PURE__ */ jsx11(Text9, { dimColor: true, children: ` ${age}` }) : null
1012
+ return /* @__PURE__ */ jsxs9(Box10, { children: [
1013
+ /* @__PURE__ */ jsx12(Text10, { color, children: ` ${glyph} ` }),
1014
+ /* @__PURE__ */ jsx12(Text10, { dimColor: !done && !running, children: label }),
1015
+ age ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${age}` }) : null
716
1016
  ] });
717
1017
  }
718
1018
  var init_JobDetailView = __esm({
@@ -724,8 +1024,8 @@ var init_JobDetailView = __esm({
724
1024
 
725
1025
  // src/tui/RecordingDetailView.tsx
726
1026
  import React4, { useMemo as useMemo2, useState as useState3 } from "react";
727
- import { Box as Box10, Text as Text10, useInput as useInput3 } from "ink";
728
- import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1027
+ import { Box as Box11, Text as Text11, useInput as useInput3 } from "ink";
1028
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
729
1029
  function RecordingDetailView({
730
1030
  item,
731
1031
  nowMs,
@@ -788,20 +1088,20 @@ function RecordingDetailView({
788
1088
  else if (input === "g") setScroll(0);
789
1089
  else if (input === "G") setScroll(segWin.maxScroll);
790
1090
  });
791
- return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
792
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "\u2039 Recordings" }),
793
- /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { bold: true, color: "magenta", children: title }) }),
794
- /* @__PURE__ */ jsxs9(Text10, { children: [
795
- /* @__PURE__ */ jsx12(Text10, { color: style.color, children: `${style.glyph} ${style.label}` }),
796
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
1091
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
1092
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "\u2039 Recordings" }),
1093
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { bold: true, color: "magenta", children: title }) }),
1094
+ /* @__PURE__ */ jsxs10(Text11, { children: [
1095
+ /* @__PURE__ */ jsx13(Text11, { color: style.color, children: `${style.glyph} ${style.label}` }),
1096
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
797
1097
  ] }),
798
- meta3 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: meta3 }) : null,
799
- /* @__PURE__ */ jsx12(AudioActionRow, { item, audio }),
800
- !item.activeTranscriptId ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs9(Fragment2, { children: [
801
- /* @__PURE__ */ jsx12(TabBar, { active: tab }),
802
- /* @__PURE__ */ jsx12(Box10, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx12(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx12(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx12(TranscriptPane, { segments, win: segWin }) })
1098
+ meta3 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: meta3 }) : null,
1099
+ /* @__PURE__ */ jsx13(AudioActionRow, { item, audio }),
1100
+ !item.activeTranscriptId ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
1101
+ /* @__PURE__ */ jsx13(TabBar, { active: tab }),
1102
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx13(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx13(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx13(TranscriptPane, { segments, win: segWin }) })
803
1103
  ] }),
804
- /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
1104
+ /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
805
1105
  ready ? "tab switch" : "",
806
1106
  ready && tab === "chapters" ? " \xB7 \u2191\u2193 select \xB7 \u23CE jump" : "",
807
1107
  scrollable ? " \xB7 \u2191\u2193 scroll" : "",
@@ -814,9 +1114,9 @@ function RecordingDetailView({
814
1114
  ] });
815
1115
  }
816
1116
  function TabBar({ active }) {
817
- return /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: TAB_ORDER.map((tab, i) => /* @__PURE__ */ jsxs9(React4.Fragment, { children: [
818
- i > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " " }) : null,
819
- tab === active ? /* @__PURE__ */ jsx12(Text10, { inverse: true, bold: true, children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
1117
+ return /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: TAB_ORDER.map((tab, i) => /* @__PURE__ */ jsxs10(React4.Fragment, { children: [
1118
+ i > 0 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " " }) : null,
1119
+ tab === active ? /* @__PURE__ */ jsx13(Text11, { inverse: true, bold: true, children: ` ${TAB_LABEL[tab]} ` }) : /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${TAB_LABEL[tab]} ` })
820
1120
  ] }, tab)) });
821
1121
  }
822
1122
  function AudioActionRow({
@@ -827,30 +1127,30 @@ function AudioActionRow({
827
1127
  const status = audio?.status ?? "idle";
828
1128
  let line;
829
1129
  if (!ready) {
830
- line = /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "Audio available once the recording is ready" });
1130
+ line = /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Audio available once the recording is ready" });
831
1131
  } else if (status === "downloading") {
832
- line = /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "Downloading audio\u2026" });
1132
+ line = /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "Downloading audio\u2026" });
833
1133
  } else if (status === "opening") {
834
- line = /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "Opening\u2026" });
1134
+ line = /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "Opening\u2026" });
835
1135
  } else if (status === "error") {
836
- line = /* @__PURE__ */ jsx12(Text10, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
1136
+ line = /* @__PURE__ */ jsx13(Text11, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
837
1137
  } else if (status === "ready" && audio?.localPath) {
838
- line = /* @__PURE__ */ jsxs9(Text10, { children: [
839
- /* @__PURE__ */ jsx12(Text10, { color: "green", children: "\u2713 Downloaded " }),
840
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
1138
+ line = /* @__PURE__ */ jsxs10(Text11, { children: [
1139
+ /* @__PURE__ */ jsx13(Text11, { color: "green", children: "\u2713 Downloaded " }),
1140
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
841
1141
  ] });
842
1142
  } else {
843
- line = /* @__PURE__ */ jsxs9(Text10, { children: [
844
- /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "o" }),
845
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " open in player \xB7 " }),
846
- /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "d" }),
847
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " download \xB7 " }),
848
- /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: "f" }),
849
- /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: " reveal in Finder" })
1143
+ line = /* @__PURE__ */ jsxs10(Text11, { children: [
1144
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "o" }),
1145
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " open in player \xB7 " }),
1146
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "d" }),
1147
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " download \xB7 " }),
1148
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: "f" }),
1149
+ /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: " reveal in Finder" })
850
1150
  ] });
851
1151
  }
852
- return /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
853
- /* @__PURE__ */ jsx12(Text10, { color: ready ? "cyan" : "gray", children: "\u266A " }),
1152
+ return /* @__PURE__ */ jsxs10(Box11, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
1153
+ /* @__PURE__ */ jsx13(Text11, { color: ready ? "cyan" : "gray", children: "\u266A " }),
854
1154
  line
855
1155
  ] });
856
1156
  }
@@ -859,12 +1159,12 @@ function SummaryPane({
859
1159
  budget
860
1160
  }) {
861
1161
  if (!summary || summary.status !== "succeeded" || !summary.tldr) {
862
- return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` });
1162
+ return /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` });
863
1163
  }
864
1164
  const points = (summary.keyPoints ?? []).slice(0, Math.max(1, budget - 4));
865
- return /* @__PURE__ */ jsxs9(Fragment2, { children: [
866
- /* @__PURE__ */ jsx12(Text10, { children: summary.tldr }),
867
- points.length > 0 ? /* @__PURE__ */ jsx12(Box10, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `\u2022 ${point}` }, i)) }) : null
1165
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1166
+ /* @__PURE__ */ jsx13(Text11, { children: summary.tldr }),
1167
+ points.length > 0 ? /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: `\u2022 ${point}` }, i)) }) : null
868
1168
  ] });
869
1169
  }
870
1170
  function ChaptersPane({
@@ -872,32 +1172,32 @@ function ChaptersPane({
872
1172
  win,
873
1173
  selectedIndex
874
1174
  }) {
875
- if (chapters.length === 0) return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "No chapters" });
876
- return /* @__PURE__ */ jsxs9(Fragment2, { children: [
1175
+ if (chapters.length === 0) return /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "No chapters" });
1176
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
877
1177
  chapters.slice(win.start, win.end).map((chapter, i) => {
878
1178
  const index = win.start + i;
879
1179
  const selected = index === selectedIndex;
880
- return /* @__PURE__ */ jsxs9(Text10, { wrap: "truncate-end", children: [
881
- /* @__PURE__ */ jsx12(Text10, { color: "cyan", children: selected ? "\u25B8 " : " " }),
882
- /* @__PURE__ */ jsx12(Text10, { color: "blue", children: `[${formatClockMs(chapter.startMs)}] ` }),
883
- /* @__PURE__ */ jsx12(Text10, { bold: selected, children: chapter.title })
1180
+ return /* @__PURE__ */ jsxs10(Text11, { wrap: "truncate-end", children: [
1181
+ /* @__PURE__ */ jsx13(Text11, { color: "cyan", children: selected ? "\u25B8 " : " " }),
1182
+ /* @__PURE__ */ jsx13(Text11, { color: "blue", children: `[${formatClockMs(chapter.startMs)}] ` }),
1183
+ /* @__PURE__ */ jsx13(Text11, { bold: selected, children: chapter.title })
884
1184
  ] }, index);
885
1185
  }),
886
- win.end < chapters.length || win.start > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${selectedIndex + 1} / ${chapters.length}` }) : null
1186
+ win.end < chapters.length || win.start > 0 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${selectedIndex + 1} / ${chapters.length}` }) : null
887
1187
  ] });
888
1188
  }
889
1189
  function TranscriptPane({
890
1190
  segments,
891
1191
  win
892
1192
  }) {
893
- if (segments.length === 0) return /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: "(no segments)" });
894
- return /* @__PURE__ */ jsxs9(Fragment2, { children: [
895
- segments.slice(win.start, win.end).map((seg, i) => /* @__PURE__ */ jsxs9(Text10, { children: [
896
- /* @__PURE__ */ jsx12(Text10, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
897
- seg.speaker ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: `${seg.speaker} ` }) : null,
898
- /* @__PURE__ */ jsx12(Text10, { children: seg.text })
1193
+ if (segments.length === 0) return /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "(no segments)" });
1194
+ return /* @__PURE__ */ jsxs10(Fragment3, { children: [
1195
+ segments.slice(win.start, win.end).map((seg, i) => /* @__PURE__ */ jsxs10(Text11, { children: [
1196
+ /* @__PURE__ */ jsx13(Text11, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
1197
+ seg.speaker ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: `${seg.speaker} ` }) : null,
1198
+ /* @__PURE__ */ jsx13(Text11, { children: seg.text })
899
1199
  ] }, win.start + i)),
900
- win.maxScroll > 0 ? /* @__PURE__ */ jsx12(Text10, { dimColor: true, children: ` ${win.start + 1}\u2013${win.end} / ${segments.length}` }) : null
1200
+ win.maxScroll > 0 ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${win.start + 1}\u2013${win.end} / ${segments.length}` }) : null
901
1201
  ] });
902
1202
  }
903
1203
  var TAB_ORDER, TAB_LABEL;
@@ -918,8 +1218,8 @@ var init_RecordingDetailView = __esm({
918
1218
 
919
1219
  // src/tui/TranscriptView.tsx
920
1220
  import { useMemo as useMemo3, useState as useState4 } from "react";
921
- import { Box as Box11, Text as Text11, useInput as useInput4 } from "ink";
922
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1221
+ import { Box as Box12, Text as Text12, useInput as useInput4 } from "ink";
1222
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
923
1223
  function TranscriptView({ loading, data, error: error51 }) {
924
1224
  const size = useTerminalSize();
925
1225
  const [scroll, setScroll] = useState4(0);
@@ -944,42 +1244,42 @@ function TranscriptView({ loading, data, error: error51 }) {
944
1244
  else if (input === "G") setScroll(win.maxScroll);
945
1245
  });
946
1246
  if (loading) {
947
- return /* @__PURE__ */ jsx13(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "Loading transcript\u2026" }) });
1247
+ return /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Loading transcript\u2026" }) });
948
1248
  }
949
1249
  if (error51) {
950
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
951
- /* @__PURE__ */ jsxs10(Text11, { color: "red", children: [
1250
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1251
+ /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
952
1252
  "! ",
953
1253
  error51
954
1254
  ] }),
955
- /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "q / esc / \u2190 back" })
1255
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "q / esc / \u2190 back" })
956
1256
  ] });
957
1257
  }
958
1258
  if (!data) {
959
- return /* @__PURE__ */ jsx13(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: "No transcript." }) });
1259
+ return /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "No transcript." }) });
960
1260
  }
961
1261
  const title = data.summary?.title ?? "Transcript";
962
1262
  const total = segments.length;
963
1263
  const more = win.maxScroll > 0;
964
1264
  const position = total === 0 ? "" : `${win.start + 1}\u2013${win.end} / ${total}`;
965
- return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
966
- /* @__PURE__ */ jsxs10(Text11, { bold: true, color: "magenta", children: [
1265
+ return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1266
+ /* @__PURE__ */ jsxs11(Text12, { bold: true, color: "magenta", children: [
967
1267
  title,
968
- more ? /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: ` ${position}` }) : null
1268
+ more ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: ` ${position}` }) : null
969
1269
  ] }),
970
- /* @__PURE__ */ jsx13(Box11, { marginTop: 1, flexDirection: "column", children: total === 0 ? /* @__PURE__ */ jsx13(Text11, { children: data.text }) : segments.slice(win.start, win.end).map((segment, index) => /* @__PURE__ */ jsxs10(Text11, { children: [
971
- /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
1270
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, flexDirection: "column", children: total === 0 ? /* @__PURE__ */ jsx14(Text12, { children: data.text }) : segments.slice(win.start, win.end).map((segment, index) => /* @__PURE__ */ jsxs11(Text12, { children: [
1271
+ /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
972
1272
  "[",
973
1273
  formatClockMs(segment.startMs),
974
1274
  "] "
975
1275
  ] }),
976
- segment.speaker ? /* @__PURE__ */ jsxs10(Text11, { color: "cyan", children: [
1276
+ segment.speaker ? /* @__PURE__ */ jsxs11(Text12, { color: "cyan", children: [
977
1277
  segment.speaker,
978
1278
  ": "
979
1279
  ] }) : null,
980
1280
  segment.text
981
1281
  ] }, win.start + index)) }),
982
- /* @__PURE__ */ jsx13(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
1282
+ /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
983
1283
  more ? "\u2191\u2193 scroll \xB7 PgUp/PgDn \xB7 g/G top/bottom \xB7 " : "",
984
1284
  "q / esc / \u2190 back"
985
1285
  ] }) })
@@ -995,15 +1295,17 @@ var init_TranscriptView = __esm({
995
1295
 
996
1296
  // src/tui/AppShell.tsx
997
1297
  import { useCallback, useEffect as useEffect2, useState as useState5 } from "react";
998
- import { Box as Box12, Text as Text12, useApp, useInput as useInput5 } from "ink";
999
- import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1298
+ import { Box as Box13, Text as Text13, useApp, useInput as useInput5 } from "ink";
1299
+ import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1000
1300
  function AppShell({
1001
1301
  fetchJobs,
1002
1302
  fetchTranscript,
1003
1303
  fetchRecordings,
1004
1304
  fetchDashboardStats,
1305
+ fetchAccountStatus,
1005
1306
  recordingAudio,
1006
1307
  listDownloadedRecordingIds,
1308
+ startLiveRecord,
1007
1309
  initialView = "overview",
1008
1310
  openUrl: openUrl2,
1009
1311
  copyText: copyText2,
@@ -1018,6 +1320,7 @@ function AppShell({
1018
1320
  const [recordingsNextCursor, setRecordingsNextCursor] = useState5(null);
1019
1321
  const [recordingsTotalCount, setRecordingsTotalCount] = useState5(void 0);
1020
1322
  const [stats, setStats] = useState5(void 0);
1323
+ const [accountStatus, setAccountStatus] = useState5("loading");
1021
1324
  const [origin, setOrigin] = useState5("");
1022
1325
  const [stack, setStack] = useState5([{ kind: initialView }]);
1023
1326
  const [selected, setSelected] = useState5(0);
@@ -1031,6 +1334,7 @@ function AppShell({
1031
1334
  );
1032
1335
  const [audioCache, setAudioCache] = useState5(() => /* @__PURE__ */ new Map());
1033
1336
  const [downloadedIds, setDownloadedIds] = useState5(() => /* @__PURE__ */ new Set());
1337
+ const [liveRecord, setLiveRecord] = useState5(void 0);
1034
1338
  const refreshDownloadedIds = useCallback(async () => {
1035
1339
  if (!listDownloadedRecordingIds) return;
1036
1340
  try {
@@ -1042,6 +1346,42 @@ function AppShell({
1042
1346
  void refreshDownloadedIds();
1043
1347
  }, [refreshDownloadedIds]);
1044
1348
  const screen = stack[stack.length - 1];
1349
+ const stopLiveRecord = useCallback(async () => {
1350
+ const current = liveRecord;
1351
+ if (current?.kind === "live") {
1352
+ setLiveRecord({ kind: "starting" });
1353
+ try {
1354
+ await current.session.stop();
1355
+ } catch (error51) {
1356
+ setNotice(error51 instanceof Error ? error51.message : String(error51));
1357
+ }
1358
+ }
1359
+ setLiveRecord(void 0);
1360
+ setStack([{ kind: "overview" }]);
1361
+ }, [liveRecord]);
1362
+ useEffect2(() => {
1363
+ if (screen.kind !== "record") return;
1364
+ if (!startLiveRecord) {
1365
+ setLiveRecord({ kind: "error", message: "Live recording is not available" });
1366
+ return;
1367
+ }
1368
+ let cancelled = false;
1369
+ setLiveRecord({ kind: "starting" });
1370
+ startLiveRecord().then((session) => {
1371
+ if (cancelled) {
1372
+ void session.stop();
1373
+ return;
1374
+ }
1375
+ setLiveRecord({ kind: "live", session });
1376
+ }).catch((error51) => {
1377
+ if (!cancelled) {
1378
+ setLiveRecord({ kind: "error", message: error51 instanceof Error ? error51.message : String(error51) });
1379
+ }
1380
+ });
1381
+ return () => {
1382
+ cancelled = true;
1383
+ };
1384
+ }, [screen.kind, startLiveRecord]);
1045
1385
  const selectedRecording = screen.kind === "overview" ? recordings[selected] : void 0;
1046
1386
  const peekTranscriptId = selectedRecording?.activeTranscriptId ?? void 0;
1047
1387
  useEffect2(() => {
@@ -1062,10 +1402,11 @@ function AppShell({
1062
1402
  }, [peekTranscriptId, fetchTranscript]);
1063
1403
  const peekSummary = peekTranscriptId ? summaryCache.get(peekTranscriptId) : void 0;
1064
1404
  const refresh = useCallback(async ({ resetRecordings = false } = {}) => {
1065
- const [jobsR, recR, statsR] = await Promise.allSettled([
1405
+ const [jobsR, recR, statsR, accountR] = await Promise.allSettled([
1066
1406
  fetchJobs(),
1067
1407
  resetRecordings && fetchRecordings ? fetchRecordings({ limit: RECORDINGS_PAGE_SIZE }) : Promise.resolve(void 0),
1068
- fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0)
1408
+ fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0),
1409
+ fetchAccountStatus ? fetchAccountStatus() : Promise.resolve(void 0)
1069
1410
  ]);
1070
1411
  if (jobsR.status === "fulfilled") {
1071
1412
  setJobs(jobsR.value.items);
@@ -1080,7 +1421,12 @@ function AppShell({
1080
1421
  setRecordingsTotalCount(recR.value.totalCount);
1081
1422
  }
1082
1423
  if (statsR.status === "fulfilled" && statsR.value) setStats(statsR.value);
1083
- }, [fetchJobs, fetchRecordings, fetchDashboardStats]);
1424
+ if (accountR.status === "fulfilled") {
1425
+ setAccountStatus(accountR.value);
1426
+ } else {
1427
+ setAccountStatus("error");
1428
+ }
1429
+ }, [fetchJobs, fetchRecordings, fetchDashboardStats, fetchAccountStatus]);
1084
1430
  const loadMoreRecordings = useCallback(async () => {
1085
1431
  if (!fetchRecordings || !recordingsNextCursor || loadingMoreRecordings) return;
1086
1432
  setLoadingMoreRecordings(true);
@@ -1125,7 +1471,7 @@ function AppShell({
1125
1471
  jobStatusByRecording.set(job.recordingId, job.status);
1126
1472
  }
1127
1473
  }
1128
- const listLength = screen.kind === "jobs" ? jobs.length : recordings.length;
1474
+ const listLength = screen.kind === "jobs" ? jobs.length : screen.kind === "overview" ? recordings.length : 0;
1129
1475
  useEffect2(() => {
1130
1476
  setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
1131
1477
  }, [listLength]);
@@ -1212,10 +1558,16 @@ function AppShell({
1212
1558
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
1213
1559
  useInput5((input, key) => {
1214
1560
  setNotice(void 0);
1561
+ if (screen.kind === "record") {
1562
+ if (input === "q" || key.escape || key.leftArrow) void stopLiveRecord();
1563
+ return;
1564
+ }
1215
1565
  if (input === "q") return exit();
1216
1566
  if (key.escape || key.leftArrow) return back();
1217
1567
  if (input === "1") return goTab("overview");
1218
1568
  if (input === "2") return goTab("jobs");
1569
+ if (input === "3") return goTab("account");
1570
+ if (input === "4") return goTab("record");
1219
1571
  if (input === "r") return void refresh({ resetRecordings: true });
1220
1572
  if (screen.kind === "overview") {
1221
1573
  if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
@@ -1262,18 +1614,18 @@ function AppShell({
1262
1614
  }
1263
1615
  });
1264
1616
  if (screen.kind === "transcript") {
1265
- return /* @__PURE__ */ jsx14(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1617
+ return /* @__PURE__ */ jsx15(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1266
1618
  }
1267
1619
  if (screen.kind === "jobDetail") {
1268
1620
  const job = jobs.find((j) => j.jobId === screen.jobId);
1269
- if (!job) return /* @__PURE__ */ jsx14(Missing, { label: "Job" });
1270
- return /* @__PURE__ */ jsx14(Detail, { notice, children: /* @__PURE__ */ jsx14(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1621
+ if (!job) return /* @__PURE__ */ jsx15(Missing, { label: "Job" });
1622
+ return /* @__PURE__ */ jsx15(Detail, { notice, children: /* @__PURE__ */ jsx15(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1271
1623
  }
1272
1624
  if (screen.kind === "recordingDetail") {
1273
1625
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
1274
- if (!rec) return /* @__PURE__ */ jsx14(Missing, { label: "Recording" });
1626
+ if (!rec) return /* @__PURE__ */ jsx15(Missing, { label: "Recording" });
1275
1627
  const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
1276
- return /* @__PURE__ */ jsx14(Detail, { notice, children: /* @__PURE__ */ jsx14(
1628
+ return /* @__PURE__ */ jsx15(Detail, { notice, children: /* @__PURE__ */ jsx15(
1277
1629
  RecordingDetailView,
1278
1630
  {
1279
1631
  item: rec,
@@ -1283,7 +1635,20 @@ function AppShell({
1283
1635
  }
1284
1636
  ) });
1285
1637
  }
1286
- const tab = screen.kind === "jobs" ? "jobs" : "overview";
1638
+ if (screen.kind === "record") {
1639
+ if (liveRecord?.kind === "live") {
1640
+ return /* @__PURE__ */ jsx15(LiveCaptionsScreen, { source: liveRecord.session.source, now });
1641
+ }
1642
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1643
+ /* @__PURE__ */ jsx15(Header, { active: "record" }),
1644
+ /* @__PURE__ */ jsx15(Box13, { flexGrow: 1, flexDirection: "column", paddingX: 1, paddingTop: 1, children: liveRecord?.kind === "error" ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
1645
+ /* @__PURE__ */ jsx15(Text13, { color: "red", children: "Couldn't start live recording" }),
1646
+ /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: liveRecord.message })
1647
+ ] }) : /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: "Starting live recording\u2026" }) }),
1648
+ /* @__PURE__ */ jsx15(Footer, { keys: "q / esc / \u2190 back" })
1649
+ ] });
1650
+ }
1651
+ const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "account" ? "account" : "overview";
1287
1652
  let body;
1288
1653
  let position = "";
1289
1654
  if (screen.kind === "overview") {
@@ -1298,7 +1663,7 @@ function AppShell({
1298
1663
  const showPeek = size.columns >= 100;
1299
1664
  const peekWidth = showPeek ? 34 : 0;
1300
1665
  const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
1301
- body = /* @__PURE__ */ jsx14(
1666
+ body = /* @__PURE__ */ jsx15(
1302
1667
  OverviewView,
1303
1668
  {
1304
1669
  recordings: recordings.slice(win.start, win.end),
@@ -1316,10 +1681,13 @@ function AppShell({
1316
1681
  peekWidth
1317
1682
  }
1318
1683
  );
1684
+ } else if (screen.kind === "account") {
1685
+ position = "";
1686
+ body = /* @__PURE__ */ jsx15(AccountView, { status: accountStatus });
1319
1687
  } else {
1320
1688
  const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
1321
1689
  position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
1322
- body = /* @__PURE__ */ jsx14(
1690
+ body = /* @__PURE__ */ jsx15(
1323
1691
  JobsView,
1324
1692
  {
1325
1693
  items: jobs.slice(win.start, win.end),
@@ -1328,47 +1696,49 @@ function AppShell({
1328
1696
  }
1329
1697
  );
1330
1698
  }
1331
- const footerKeys = screen.kind === "jobs" ? `${position} \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 1 overview \xB7 r refresh \xB7 q quit` : `${position} \xB7 \u2191\u2193 scroll \xB7 \u23CE open \xB7 t transcript \xB7 2 jobs \xB7 r refresh \xB7 q quit`;
1332
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1333
- /* @__PURE__ */ jsx14(Header, { active: tab }),
1334
- /* @__PURE__ */ jsxs11(Box12, { flexGrow: 1, flexDirection: "column", children: [
1699
+ const footerKeys = screen.kind === "jobs" ? `${position} \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 1 overview \xB7 3 account \xB7 4 record \xB7 r refresh \xB7 q quit` : screen.kind === "account" ? "3 account \xB7 1 overview \xB7 2 jobs \xB7 4 record \xB7 r refresh \xB7 q quit" : `${position} \xB7 \u2191\u2193 scroll \xB7 \u23CE open \xB7 t transcript \xB7 2 jobs \xB7 3 account \xB7 4 record \xB7 r refresh \xB7 q quit`;
1700
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1701
+ /* @__PURE__ */ jsx15(Header, { active: tab }),
1702
+ /* @__PURE__ */ jsxs12(Box13, { flexGrow: 1, flexDirection: "column", children: [
1335
1703
  body,
1336
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: "red", children: [
1704
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text13, { color: "red", children: [
1337
1705
  "! ",
1338
1706
  loadError
1339
1707
  ] }) }) : null
1340
1708
  ] }),
1341
- /* @__PURE__ */ jsx14(Footer, { keys: footerKeys })
1709
+ /* @__PURE__ */ jsx15(Footer, { keys: footerKeys })
1342
1710
  ] });
1343
1711
  }
1344
1712
  function Detail({
1345
1713
  notice,
1346
1714
  children
1347
1715
  }) {
1348
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", children: [
1716
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", children: [
1349
1717
  children,
1350
- notice ? /* @__PURE__ */ jsx14(Box12, { paddingX: 1, children: /* @__PURE__ */ jsx14(Text12, { color: "green", children: notice }) }) : null
1718
+ notice ? /* @__PURE__ */ jsx15(Box13, { paddingX: 1, children: /* @__PURE__ */ jsx15(Text13, { color: "green", children: notice }) }) : null
1351
1719
  ] });
1352
1720
  }
1353
1721
  function Missing({ label }) {
1354
- return /* @__PURE__ */ jsxs11(Box12, { flexDirection: "column", paddingX: 1, children: [
1355
- /* @__PURE__ */ jsxs11(Text12, { dimColor: true, children: [
1722
+ return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 1, children: [
1723
+ /* @__PURE__ */ jsxs12(Text13, { dimColor: true, children: [
1356
1724
  label,
1357
1725
  " no longer in the list."
1358
1726
  ] }),
1359
- /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "esc back \xB7 q quit" })
1727
+ /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: "esc back \xB7 q quit" })
1360
1728
  ] });
1361
1729
  }
1362
1730
  var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
1363
1731
  var init_AppShell = __esm({
1364
1732
  "src/tui/AppShell.tsx"() {
1365
1733
  "use strict";
1734
+ init_AccountView();
1366
1735
  init_chrome();
1367
1736
  init_JobsView();
1368
1737
  init_OverviewView();
1369
1738
  init_JobDetailView();
1370
1739
  init_RecordingDetailView();
1371
1740
  init_TranscriptView();
1741
+ init_LiveCaptionsScreen();
1372
1742
  init_format();
1373
1743
  init_terminal();
1374
1744
  RECORDINGS_PAGE_SIZE = 50;
@@ -1413,8 +1783,10 @@ async function runDashboard(deps) {
1413
1783
  fetchTranscript: deps.fetchTranscript,
1414
1784
  fetchRecordings: deps.fetchRecordings,
1415
1785
  fetchDashboardStats: deps.fetchDashboardStats,
1786
+ fetchAccountStatus: deps.fetchAccountStatus,
1416
1787
  recordingAudio: deps.recordingAudio,
1417
1788
  listDownloadedRecordingIds: deps.listDownloadedRecordingIds,
1789
+ startLiveRecord: deps.startLiveRecord,
1418
1790
  initialView: deps.initialView ?? "overview",
1419
1791
  openUrl,
1420
1792
  copyText
@@ -19145,227 +19517,64 @@ function defaultSidecarHandshakeParams(params) {
19145
19517
  };
19146
19518
  }
19147
19519
 
19148
- // src/tui/LiveCaptionsScreen.tsx
19149
- import { useEffect, useState as useState2 } from "react";
19150
-
19151
- // src/tui/LiveCaptionsView.tsx
19152
- init_format();
19153
- init_terminal();
19154
- import { useMemo, useState } from "react";
19155
- import { Box, Text, useInput } from "ink";
19156
-
19157
- // src/tui/liveCaptions.ts
19158
- function initialLiveCaptionsState() {
19159
- return { status: "connecting", lines: [] };
19160
- }
19161
- function liveCaptionReducer(state, event) {
19162
- switch (event.kind) {
19163
- case "status": {
19164
- const next = { ...state, status: event.status };
19165
- if (event.status === "live" && state.startedAtMs == null) {
19166
- next.startedAtMs = event.atMs ?? state.startedAtMs;
19167
- }
19168
- if (event.status !== "error") next.error = void 0;
19169
- return next;
19170
- }
19171
- case "partial":
19172
- return { ...state, partial: event.text };
19173
- case "final": {
19174
- const lines = [...state.lines, event.line];
19175
- return { ...state, lines, partial: void 0 };
19520
+ // src/record.tsx
19521
+ init_LiveCaptionsScreen();
19522
+ import { jsx as jsx3 } from "react/jsx-runtime";
19523
+ var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";
19524
+ async function recordViaSidecar(opts) {
19525
+ let liveRenderer;
19526
+ let session;
19527
+ try {
19528
+ session = await startRecordSession(opts);
19529
+ if (opts.renderLive) {
19530
+ liveRenderer = opts.runtime?.createLiveRenderer?.(session.source) ?? createInkLiveRenderer({
19531
+ source: session.source,
19532
+ renderApp: opts.runtime?.renderApp,
19533
+ now: opts.runtime?.now
19534
+ });
19176
19535
  }
19177
- case "translationPartial":
19178
- return { ...state, translationPartial: event.text };
19179
- case "translationFinal": {
19180
- const lines = [...state.lines];
19181
- let idx = event.segmentId ? lines.findIndex((l) => l.id === event.segmentId) : -1;
19182
- if (idx < 0) idx = lines.length - 1;
19183
- if (idx >= 0) lines[idx] = { ...lines[idx], translation: event.text };
19184
- return { ...state, lines, translationPartial: void 0 };
19536
+ if (liveRenderer) {
19537
+ await liveRenderer.waitUntilStop();
19538
+ } else {
19539
+ await (opts.runtime?.waitForStop ?? waitForStopSignal)();
19185
19540
  }
19186
- case "error":
19187
- return { ...state, status: "error", error: event.message };
19188
- default:
19189
- return state;
19190
- }
19191
- }
19192
- function recordingStateToStatus(state) {
19193
- switch (state) {
19194
- case "idle":
19195
- case "starting":
19196
- return "connecting";
19197
- case "recording":
19198
- return "live";
19199
- case "stopping":
19200
- case "finalizing":
19201
- case "uploading":
19202
- case "completed":
19203
- case "cancelled":
19204
- return "stopped";
19205
- case "failed":
19206
- return "error";
19207
- }
19208
- }
19209
- function sidecarToLiveCaptionEvent(event) {
19210
- switch (event.type) {
19211
- case "ready":
19212
- return { kind: "status", status: "connecting" };
19213
- case "recording.state":
19214
- if (event.state === "failed") {
19215
- return { kind: "error", message: event.message ?? "Recording failed" };
19216
- }
19217
- return { kind: "status", status: recordingStateToStatus(event.state) };
19218
- case "live_caption.delta": {
19219
- if (event.stream === "translation") {
19220
- return event.isFinal ? { kind: "translationFinal", segmentId: event.segmentId, text: event.text } : { kind: "translationPartial", text: event.text };
19221
- }
19222
- if (event.isFinal) {
19223
- return {
19224
- kind: "final",
19225
- line: {
19226
- id: event.segmentId ?? `${event.startMs ?? event.atMs ?? 0}`,
19227
- text: event.text,
19228
- speaker: event.speaker,
19229
- atMs: event.startMs ?? event.atMs
19230
- }
19231
- };
19541
+ return await session.stop();
19542
+ } catch (error51) {
19543
+ if (session) {
19544
+ try {
19545
+ await session.cancel();
19546
+ } catch {
19232
19547
  }
19233
- return { kind: "partial", text: event.text };
19234
19548
  }
19235
- case "error":
19236
- return { kind: "error", message: event.message };
19237
- case "audio.level":
19238
- case "local_artifact.upserted":
19239
- return null;
19240
- default:
19241
- return null;
19242
- }
19243
- }
19244
- function liveCaptionStatusLabel(status) {
19245
- switch (status) {
19246
- case "connecting":
19247
- return "Connecting\u2026";
19248
- case "live":
19249
- return "\u25CF LIVE";
19250
- case "reconnecting":
19251
- return "Reconnecting\u2026";
19252
- case "stopped":
19253
- return "Stopped";
19254
- case "error":
19255
- return "Error";
19549
+ throw error51;
19550
+ } finally {
19551
+ liveRenderer?.close();
19552
+ session?.close();
19256
19553
  }
19257
19554
  }
19258
-
19259
- // src/tui/LiveCaptionsView.tsx
19260
- import { jsx, jsxs } from "react/jsx-runtime";
19261
- var STATUS_COLOR = {
19262
- connecting: "yellow",
19263
- live: "red",
19264
- reconnecting: "yellow",
19265
- stopped: "gray",
19266
- error: "red"
19267
- };
19268
- function LiveCaptionsView({
19269
- state,
19270
- nowMs
19271
- }) {
19272
- const size = useTerminalSize();
19273
- const [scrollUp, setScrollUp] = useState(0);
19274
- const innerWidth = Math.max(10, size.columns - 2);
19275
- const items = useMemo(() => {
19276
- const rows = [];
19277
- for (const l of state.lines) {
19278
- rows.push({ key: l.id, kind: "final", speaker: l.speaker, text: l.text });
19279
- if (l.translation) rows.push({ key: `${l.id}__t`, kind: "translation", text: l.translation });
19280
- }
19281
- if (state.partial && state.partial.length > 0) {
19282
- rows.push({ key: "__partial__", kind: "partial", text: state.partial });
19283
- }
19284
- if (state.translationPartial && state.translationPartial.length > 0) {
19285
- rows.push({ key: "__tpartial__", kind: "translation", text: state.translationPartial });
19555
+ async function startLiveRecordSession(opts) {
19556
+ const session = await startRecordSession({ ...opts, live: true });
19557
+ return {
19558
+ source: session.source,
19559
+ stop: async () => {
19560
+ await session.stop();
19286
19561
  }
19287
- return rows;
19288
- }, [state.lines, state.partial, state.translationPartial]);
19289
- const heights = useMemo(
19290
- () => items.map((it) => {
19291
- const prefix = it.kind === "translation" ? "\u21B3 " : it.speaker ? `${it.speaker}: ` : "";
19292
- return Math.max(1, Math.ceil(displayWidth(prefix + it.text) / innerWidth));
19293
- }),
19294
- [items, innerWidth]
19295
- );
19296
- const budget = Math.max(3, size.rows - 3);
19297
- const maxScroll = windowByHeights(heights, Number.MAX_SAFE_INTEGER, budget).maxScroll;
19298
- const top = Math.max(0, maxScroll - scrollUp);
19299
- const win = windowByHeights(heights, top, budget);
19300
- const following = scrollUp === 0;
19301
- const page = Math.max(1, budget - 1);
19302
- useInput((input, key) => {
19303
- if (key.upArrow || input === "k") setScrollUp((s) => Math.min(maxScroll, s + 1));
19304
- else if (key.downArrow || input === "j") setScrollUp((s) => Math.max(0, s - 1));
19305
- else if (key.pageUp || input === "b") setScrollUp((s) => Math.min(maxScroll, s + page));
19306
- else if (key.pageDown || input === " ") setScrollUp((s) => Math.max(0, s - page));
19307
- else if (input === "G") setScrollUp(0);
19308
- else if (input === "g") setScrollUp(maxScroll);
19309
- });
19310
- const elapsed = state.startedAtMs != null ? formatClockMs(Math.max(0, nowMs - state.startedAtMs)) : null;
19311
- const statusColor = STATUS_COLOR[state.status] ?? "white";
19312
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
19313
- /* @__PURE__ */ jsxs(Text, { children: [
19314
- /* @__PURE__ */ jsx(Text, { bold: true, color: statusColor, children: liveCaptionStatusLabel(state.status) }),
19315
- elapsed ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` ${elapsed}` }) : null,
19316
- !following ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u23F8 scrolled \u2014 G for live" }) : null
19317
- ] }),
19318
- /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: items.length === 0 ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: state.status === "error" ? state.error ? `Error: ${state.error}` : "Live captions error" : "Waiting for captions\u2026" }) : items.slice(win.start, win.end).map(
19319
- (it) => it.kind === "translation" ? /* @__PURE__ */ jsx(Text, { dimColor: true, italic: true, children: `\u21B3 ${it.text}` }, it.key) : /* @__PURE__ */ jsxs(Text, { dimColor: it.kind === "partial", italic: it.kind === "partial", children: [
19320
- it.speaker ? /* @__PURE__ */ jsx(Text, { color: "cyan", children: `${it.speaker}: ` }) : null,
19321
- it.text
19322
- ] }, it.key)
19323
- ) }),
19324
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
19325
- maxScroll > 0 ? "\u2191\u2193 scroll \xB7 G live \xB7 " : "",
19326
- "q / esc / \u2190 back"
19327
- ] }) })
19328
- ] });
19329
- }
19330
-
19331
- // src/tui/LiveCaptionsScreen.tsx
19332
- import { jsx as jsx2 } from "react/jsx-runtime";
19333
- function LiveCaptionsScreen({
19334
- source,
19335
- now = () => Date.now()
19336
- }) {
19337
- const [state, setState] = useState2(initialLiveCaptionsState);
19338
- const [tick, setTick] = useState2(() => now());
19339
- useEffect(() => {
19340
- const unsubscribe = source.onEvent((event) => {
19341
- const mapped = sidecarToLiveCaptionEvent(event);
19342
- if (mapped) setState((s) => liveCaptionReducer(s, mapped));
19343
- });
19344
- return unsubscribe;
19345
- }, [source]);
19346
- useEffect(() => {
19347
- const id = setInterval(() => setTick(now()), 1e3);
19348
- return () => clearInterval(id);
19349
- }, []);
19350
- return /* @__PURE__ */ jsx2(LiveCaptionsView, { state, nowMs: tick });
19562
+ };
19351
19563
  }
19352
-
19353
- // src/record.tsx
19354
- import { jsx as jsx3 } from "react/jsx-runtime";
19355
- var SIDECAR_COMMAND_ENV = "RECAPPI_MINI_SIDECAR";
19356
- async function recordViaSidecar(opts) {
19564
+ async function startRecordSession(opts) {
19357
19565
  const command = resolveSidecarCommand(opts);
19358
19566
  const sidecarArgs = opts.sidecarArgs ?? [];
19359
19567
  const spawnSidecar = opts.runtime?.spawnSidecar ?? spawnMiniSidecar;
19360
19568
  const sidecar = spawnSidecar({ command, args: sidecarArgs, env: opts.env });
19361
19569
  const account = requireAccountPartition(opts.account);
19362
19570
  const artifacts = [];
19363
- let liveRenderer;
19364
19571
  let handshake;
19365
19572
  let sessionId;
19366
19573
  let latestState;
19367
19574
  let recordingId;
19368
19575
  let localSessionRef;
19576
+ let stopPromise;
19577
+ let closed = false;
19369
19578
  const unsubscribe = sidecar.client.onEvent((event) => {
19370
19579
  if (event.type === "recording.state") {
19371
19580
  latestState = event.state;
@@ -19376,14 +19585,22 @@ async function recordViaSidecar(opts) {
19376
19585
  artifacts.push(event.artifact);
19377
19586
  }
19378
19587
  });
19379
- try {
19380
- if (opts.renderLive) {
19381
- liveRenderer = opts.runtime?.createLiveRenderer?.(sidecar.client) ?? createInkLiveRenderer({
19382
- source: sidecar.client,
19383
- renderApp: opts.runtime?.renderApp,
19384
- now: opts.runtime?.now
19385
- });
19588
+ const close = () => {
19589
+ if (closed) return;
19590
+ closed = true;
19591
+ unsubscribe();
19592
+ sidecar.kill();
19593
+ };
19594
+ const cancel = async () => {
19595
+ if (sessionId && latestState && latestState !== "completed" && latestState !== "cancelled") {
19596
+ try {
19597
+ await sidecar.client.cancelRecording({ sessionId });
19598
+ } catch {
19599
+ }
19386
19600
  }
19601
+ close();
19602
+ };
19603
+ try {
19387
19604
  handshake = await sidecar.client.handshake(
19388
19605
  defaultSidecarHandshakeParams({
19389
19606
  client: { name: "recappi-cli", version: opts.cliVersion },
@@ -19405,41 +19622,41 @@ async function recordViaSidecar(opts) {
19405
19622
  sessionId = started.sessionId;
19406
19623
  latestState = started.state;
19407
19624
  localSessionRef = started.localSessionRef;
19408
- if (liveRenderer) {
19409
- await liveRenderer.waitUntilStop();
19410
- } else {
19411
- await (opts.runtime?.waitForStop ?? waitForStopSignal)();
19412
- }
19413
- const stopped = await sidecar.client.stopRecording({ sessionId });
19414
- latestState = stopped.state;
19415
- recordingId = stopped.recordingId ?? recordingId;
19416
- localSessionRef = stopped.localSessionRef ?? localSessionRef;
19417
- artifacts.push(...stopped.artifacts ?? []);
19418
- const uniqueArtifacts = dedupeArtifacts(artifacts);
19419
- persistArtifacts(uniqueArtifacts, account, opts);
19420
- return recordCommandDataSchema.parse({
19421
- origin: account.backendOrigin,
19422
- userId: account.userId,
19423
- live: opts.live === true,
19424
- sessionId: stopped.sessionId,
19425
- state: stopped.state,
19426
- ...recordingId ? { recordingId } : {},
19427
- ...localSessionRef ? { localSessionRef } : {},
19428
- ...handshake?.sidecar ? { sidecar: handshake.sidecar } : {},
19429
- artifacts: uniqueArtifacts
19430
- });
19625
+ return {
19626
+ source: sidecar.client,
19627
+ stop: () => {
19628
+ stopPromise ??= (async () => {
19629
+ try {
19630
+ const stopped = await sidecar.client.stopRecording({ sessionId });
19631
+ latestState = stopped.state;
19632
+ recordingId = stopped.recordingId ?? recordingId;
19633
+ localSessionRef = stopped.localSessionRef ?? localSessionRef;
19634
+ artifacts.push(...stopped.artifacts ?? []);
19635
+ const uniqueArtifacts = dedupeArtifacts(artifacts);
19636
+ persistArtifacts(uniqueArtifacts, account, opts);
19637
+ return recordCommandDataSchema.parse({
19638
+ origin: account.backendOrigin,
19639
+ userId: account.userId,
19640
+ live: opts.live === true,
19641
+ sessionId: stopped.sessionId,
19642
+ state: stopped.state,
19643
+ ...recordingId ? { recordingId } : {},
19644
+ ...localSessionRef ? { localSessionRef } : {},
19645
+ ...handshake?.sidecar ? { sidecar: handshake.sidecar } : {},
19646
+ artifacts: uniqueArtifacts
19647
+ });
19648
+ } finally {
19649
+ close();
19650
+ }
19651
+ })();
19652
+ return stopPromise;
19653
+ },
19654
+ cancel,
19655
+ close
19656
+ };
19431
19657
  } catch (error51) {
19432
- if (sessionId && latestState && latestState !== "completed" && latestState !== "cancelled") {
19433
- try {
19434
- await sidecar.client.cancelRecording({ sessionId });
19435
- } catch {
19436
- }
19437
- }
19658
+ await cancel();
19438
19659
  throw error51;
19439
- } finally {
19440
- unsubscribe();
19441
- liveRenderer?.close();
19442
- sidecar.kill();
19443
19660
  }
19444
19661
  }
19445
19662
  function resolveSidecarCommand(opts) {
@@ -19578,10 +19795,30 @@ async function runCli(deps = {}) {
19578
19795
  fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
19579
19796
  fetchRecordings: ({ cursor, limit = DASHBOARD_RECORDINGS_PAGE_SIZE } = {}) => client.listRecordings({ limit, cursor }),
19580
19797
  fetchDashboardStats: () => client.dashboardStats(),
19798
+ fetchAccountStatus: () => client.accountStatus(),
19581
19799
  fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
19582
19800
  recordingAudio,
19583
19801
  listDownloadedRecordingIds: () => recordingAudio.listDownloadedRecordingIds(),
19584
19802
  listDownloads: () => recordingAudio.listDownloads(),
19803
+ startLiveRecord: async () => {
19804
+ const liveStatus = await client.authStatus();
19805
+ if (!liveStatus.loggedIn || !liveStatus.userId) {
19806
+ throw cliError("auth.not_logged_in", "Sign in before starting a sidecar recording.", {
19807
+ hint: "Run recappi auth login, or import the Recappi Mini session with recappi auth import-macos."
19808
+ });
19809
+ }
19810
+ return startLiveRecordSession({
19811
+ account: {
19812
+ backendOrigin: auth.origin,
19813
+ userId: liveStatus.userId,
19814
+ ...liveStatus.email ? { email: liveStatus.email } : {}
19815
+ },
19816
+ cliVersion: CLI_VERSION,
19817
+ env: deps.env,
19818
+ homeDir: deps.homeDir,
19819
+ runtime: deps.recordRuntime
19820
+ });
19821
+ },
19585
19822
  initialView: parsed.initialView
19586
19823
  });
19587
19824
  return 0;