recappi 0.1.2 → 0.1.3

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
@@ -18,22 +18,20 @@ function Header({ active }) {
18
18
  "Recappi",
19
19
  " "
20
20
  ] }),
21
- /* @__PURE__ */ jsx(Tab, { num: "1", label: "Overview", active: active === "overview", enabled: true }),
22
- /* @__PURE__ */ jsx(Tab, { num: "2", label: "Jobs", active: active === "jobs", enabled: true }),
23
- /* @__PURE__ */ jsx(Tab, { num: "3", label: "Recordings", active: active === "recordings", enabled: true })
21
+ /* @__PURE__ */ jsx(Tab, { num: "1", label: "Overview", active: active === "overview" }),
22
+ /* @__PURE__ */ jsx(Tab, { num: "2", label: "Jobs", active: active === "jobs" })
24
23
  ] });
25
24
  }
26
25
  function Tab({
27
26
  num,
28
27
  label,
29
- active,
30
- enabled
28
+ active
31
29
  }) {
32
- const text = ` ${num} ${label}${enabled ? "" : " (soon)"} `;
30
+ const text = ` ${num} ${label} `;
33
31
  if (active) {
34
- return /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: text });
32
+ return /* @__PURE__ */ jsx(Text, { bold: true, inverse: true, color: "cyan", children: text });
35
33
  }
36
- return /* @__PURE__ */ jsx(Text, { dimColor: !enabled, children: text });
34
+ return /* @__PURE__ */ jsx(Text, { children: text });
37
35
  }
38
36
  function Footer({ keys }) {
39
37
  return /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: keys }) });
@@ -80,6 +78,23 @@ function statusStyle(status) {
80
78
  return { label: status, color: "white" };
81
79
  }
82
80
  }
81
+ function spinnerChar(frame) {
82
+ return SPINNER_FRAMES[frame % SPINNER_FRAMES.length];
83
+ }
84
+ function dateBucket(epochMs, nowMs) {
85
+ if (!epochMs) return "Earlier";
86
+ const startOfDay = (ms) => {
87
+ const d = new Date(ms);
88
+ d.setHours(0, 0, 0, 0);
89
+ return d.getTime();
90
+ };
91
+ const days = Math.floor((startOfDay(nowMs) - startOfDay(epochMs)) / 864e5);
92
+ if (days <= 0) return "Today";
93
+ if (days === 1) return "Yesterday";
94
+ if (days < 7) return "Previous 7 days";
95
+ if (days < 30) return "Previous 30 days";
96
+ return "Earlier";
97
+ }
83
98
  function statusGlyph(status, spinnerFrame) {
84
99
  switch (status) {
85
100
  case "running":
@@ -114,6 +129,45 @@ function padCell(text, width) {
114
129
  if (text.length > width) return `${text.slice(0, Math.max(0, width - 1))}\u2026`;
115
130
  return text.padEnd(width);
116
131
  }
132
+ function charWidth(code) {
133
+ if (code >= 4352 && code <= 4447 || // Hangul Jamo
134
+ code === 9001 || code === 9002 || code >= 11904 && code <= 12350 || // CJK radicals … Kangxi
135
+ code >= 12353 && code <= 13311 || // Hiragana … CJK symbols
136
+ code >= 13312 && code <= 19903 || // CJK ext A
137
+ code >= 19968 && code <= 40959 || // CJK unified
138
+ code >= 40960 && code <= 42191 || // Yi
139
+ code >= 44032 && code <= 55203 || // Hangul syllables
140
+ code >= 63744 && code <= 64255 || // CJK compat
141
+ code >= 65072 && code <= 65103 || // CJK compat forms
142
+ code >= 65280 && code <= 65376 || // Fullwidth forms
143
+ code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || // emoji
144
+ code >= 131072 && code <= 262141) {
145
+ return 2;
146
+ }
147
+ return 1;
148
+ }
149
+ function displayWidth(text) {
150
+ let width = 0;
151
+ for (const ch of text) width += charWidth(ch.codePointAt(0) ?? 0);
152
+ return width;
153
+ }
154
+ function padDisplay(text, width) {
155
+ const w = displayWidth(text);
156
+ if (w === width) return text;
157
+ if (w < width) return text + " ".repeat(width - w);
158
+ let out = "";
159
+ let acc = 0;
160
+ for (const ch of text) {
161
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
162
+ if (acc + cw > width - 1) break;
163
+ out += ch;
164
+ acc += cw;
165
+ }
166
+ out += "\u2026";
167
+ acc += 1;
168
+ if (acc < width) out += " ".repeat(width - acc);
169
+ return out;
170
+ }
117
171
  function countJobs(items) {
118
172
  const counts = {
119
173
  total: items.length,
@@ -169,6 +223,30 @@ function recordingStatusStyle(status) {
169
223
  return { label: status, color: "white", glyph: "\u2022" };
170
224
  }
171
225
  }
226
+ function listWindow(selected, total, size) {
227
+ if (size <= 0 || total <= 0) return { start: 0, end: 0 };
228
+ if (total <= size) return { start: 0, end: total };
229
+ let start = selected - Math.floor(size / 2);
230
+ start = Math.max(0, Math.min(start, total - size));
231
+ return { start, end: start + size };
232
+ }
233
+ function groupedListWindow(buckets, selected, budget) {
234
+ const total = buckets.length;
235
+ if (budget <= 0 || total <= 0) return { start: 0, end: 0 };
236
+ const cost = (start, end) => {
237
+ if (end <= start) return 0;
238
+ let boundaries = 0;
239
+ for (let i = start + 1; i < end; i++) {
240
+ if (buckets[i] !== buckets[i - 1]) boundaries += 1;
241
+ }
242
+ return end - start + 1 + boundaries * 2;
243
+ };
244
+ for (let n = Math.min(total, budget); n >= 1; n--) {
245
+ const win = listWindow(selected, total, n);
246
+ if (cost(win.start, win.end) <= budget) return win;
247
+ }
248
+ return listWindow(selected, total, 1);
249
+ }
172
250
  function formatBytes2(bytes) {
173
251
  if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "";
174
252
  const units = ["B", "KB", "MB", "GB", "TB"];
@@ -246,145 +324,227 @@ var init_JobsView = __esm({
246
324
  import { Box as Box4, Text as Text4 } from "ink";
247
325
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
248
326
  function recordingTitle2(item) {
249
- return item.title || item.summaryTitle || item.recordingId;
327
+ const named = (item.title || item.summaryTitle || "").trim();
328
+ if (named && !UUID_RE.test(named)) return named;
329
+ return "Untitled";
330
+ }
331
+ function recordingProcessingState(item, jobStatus, spinnerFrame) {
332
+ if (item.status === "uploading") return { glyph: "\u2191", color: "cyan" };
333
+ if (item.status === "failed" || jobStatus === "failed") return { glyph: "\u2717", color: "red" };
334
+ if (jobStatus === "running") return { glyph: spinnerChar(spinnerFrame), color: "cyan" };
335
+ if (jobStatus === "queued") return { glyph: "\u25CB", color: "yellow" };
336
+ if (item.status === "aborted") return { glyph: "\u2022", color: "gray" };
337
+ if (item.activeTranscriptId) return { glyph: "\u2713", color: "green" };
338
+ return { glyph: "\xB7", color: "gray" };
339
+ }
340
+ function recordingLayout(columns) {
341
+ const usable = Math.max(20, columns - 2);
342
+ const showWhen = usable >= 54;
343
+ const title = Math.max(
344
+ 10,
345
+ usable - MARKER_W - GLYPH_W - LENGTH_W - (showWhen ? WHEN_W : 0)
346
+ );
347
+ return { title, showWhen };
250
348
  }
251
349
  function RecordingRow({
252
350
  item,
253
351
  selected,
254
- nowMs
352
+ nowMs,
353
+ columns,
354
+ jobStatus,
355
+ spinnerFrame = 0
255
356
  }) {
256
- const style = recordingStatusStyle(item.status);
257
- const detail = [
258
- item.durationMs ? formatClockMs(item.durationMs) : void 0,
259
- formatAge(item.createdAt, nowMs) || void 0,
260
- item.activeTranscriptId ? "transcript ready" : void 0
261
- ].filter(Boolean).join(" \xB7 ");
357
+ const { title, showWhen } = recordingLayout(columns);
358
+ const { glyph, color } = recordingProcessingState(item, jobStatus, spinnerFrame);
359
+ const duration3 = item.durationMs ? formatClockMs(item.durationMs) : "\u2014";
262
360
  return /* @__PURE__ */ jsxs3(Box4, { children: [
263
361
  /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: selected ? "\u25B8 " : " " }),
264
- /* @__PURE__ */ jsx4(Text4, { color: style.color, children: `${style.glyph} ${padCell(style.label, 10)}` }),
265
- /* @__PURE__ */ jsx4(Text4, { bold: selected, children: padCell(recordingTitle2(item), 26) }),
266
- /* @__PURE__ */ jsx4(Text4, { dimColor: !selected, children: detail })
362
+ /* @__PURE__ */ jsx4(Text4, { color, children: `${glyph} ` }),
363
+ /* @__PURE__ */ jsx4(Text4, { bold: selected, children: padDisplay(recordingTitle2(item), title) }),
364
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay(duration3, LENGTH_W) }),
365
+ showWhen ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: formatAge(item.createdAt, nowMs) }) : null
366
+ ] });
367
+ }
368
+ function RecordingHeader({ columns }) {
369
+ const { title, showWhen } = recordingLayout(columns);
370
+ return /* @__PURE__ */ jsxs3(Box4, { children: [
371
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay("", MARKER_W + GLYPH_W) }),
372
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay("TITLE", title) }),
373
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: padDisplay("LENGTH", LENGTH_W) }),
374
+ showWhen ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "WHEN" }) : null
267
375
  ] });
268
376
  }
377
+ var UUID_RE, MARKER_W, GLYPH_W, LENGTH_W, WHEN_W;
269
378
  var init_RecordingRow = __esm({
270
379
  "src/tui/RecordingRow.tsx"() {
271
380
  "use strict";
272
381
  init_format();
382
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
383
+ MARKER_W = 2;
384
+ GLYPH_W = 2;
385
+ LENGTH_W = 9;
386
+ WHEN_W = 9;
273
387
  }
274
388
  });
275
389
 
276
- // src/tui/OverviewView.tsx
390
+ // src/tui/RecordingsView.tsx
391
+ import React from "react";
277
392
  import { Box as Box5, Text as Text5 } from "ink";
278
393
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
279
- function overviewRecentRecordings(recordings) {
280
- return recordings.slice(0, RECENT_LIMIT);
281
- }
282
- function OverviewView({
283
- recordings,
284
- jobs,
285
- stats,
394
+ function RecordingsView({
395
+ items,
286
396
  selectedIndex,
287
- spinnerFrame,
288
- nowMs
397
+ nowMs,
398
+ columns,
399
+ jobStatusByRecording,
400
+ spinnerFrame = 0
289
401
  }) {
290
- const recent = overviewRecentRecordings(recordings);
291
- const activeJobs = jobs.filter((j) => j.status === "running" || j.status === "queued");
292
- const jobCounts = countJobs(jobs);
293
- const recTotal = stats?.recordings.total ?? recordings.length;
294
- const recReady = stats?.recordings.ready;
295
- const transcribed = stats?.recordings.totalDurationMs;
402
+ if (items.length === 0) {
403
+ return /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
404
+ }
296
405
  return /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
297
- /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
298
- /* @__PURE__ */ jsxs4(Text5, { children: [
299
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Recordings " }),
300
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: recTotal }),
301
- recReady != null ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${recReady} ready` }) : null,
302
- transcribed != null ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${formatClockMs(transcribed)} transcribed` }) : null
303
- ] }),
304
- /* @__PURE__ */ jsxs4(Text5, { children: [
305
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Jobs " }),
306
- /* @__PURE__ */ jsxs4(Text5, { color: "cyan", children: [
307
- stats?.jobs.running ?? jobCounts.running,
308
- " running"
309
- ] }),
310
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
311
- /* @__PURE__ */ jsxs4(Text5, { color: "yellow", children: [
312
- stats?.jobs.queued ?? jobCounts.queued,
313
- " queued"
314
- ] }),
315
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
316
- /* @__PURE__ */ jsxs4(Text5, { color: "green", children: [
317
- stats?.jobs.succeeded ?? jobCounts.succeeded,
318
- " done"
319
- ] })
320
- ] })
321
- ] }),
322
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
323
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Recent recordings" }),
324
- recent.length === 0 ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " No recordings yet \u2014 run: recappi upload <file>" }) : recent.map((item, index) => /* @__PURE__ */ jsx5(
325
- RecordingRow,
326
- {
327
- item,
328
- selected: index === selectedIndex,
329
- nowMs
330
- },
331
- item.recordingId
332
- ))
333
- ] }),
334
- activeJobs.length > 0 ? /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
335
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Transcribing now" }),
336
- activeJobs.slice(0, 3).map((job) => {
337
- const style = statusStyle(job.status);
338
- return /* @__PURE__ */ jsxs4(Box5, { children: [
339
- /* @__PURE__ */ jsx5(Text5, { children: " " }),
340
- /* @__PURE__ */ jsx5(Text5, { color: style.color, children: `${statusGlyph(job.status, spinnerFrame)} ` }),
341
- /* @__PURE__ */ jsx5(Text5, { children: job.recording?.title ?? job.recordingId })
342
- ] }, job.jobId);
343
- })
344
- ] }) : null
406
+ /* @__PURE__ */ jsx5(RecordingHeader, { columns }),
407
+ items.map((item, index) => {
408
+ const bucket = dateBucket(item.createdAt, nowMs);
409
+ const showHeader = index === 0 || bucket !== dateBucket(items[index - 1].createdAt, nowMs);
410
+ return /* @__PURE__ */ jsxs4(React.Fragment, { children: [
411
+ showHeader ? /* @__PURE__ */ jsx5(Box5, { marginTop: index === 0 ? 0 : 1, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: "blue", children: bucket }) }) : null,
412
+ /* @__PURE__ */ jsx5(
413
+ RecordingRow,
414
+ {
415
+ item,
416
+ selected: index === selectedIndex,
417
+ nowMs,
418
+ columns,
419
+ jobStatus: jobStatusByRecording?.get(item.recordingId),
420
+ spinnerFrame
421
+ }
422
+ )
423
+ ] }, item.recordingId);
424
+ })
345
425
  ] });
346
426
  }
347
- var RECENT_LIMIT;
348
- var init_OverviewView = __esm({
349
- "src/tui/OverviewView.tsx"() {
427
+ var init_RecordingsView = __esm({
428
+ "src/tui/RecordingsView.tsx"() {
350
429
  "use strict";
351
430
  init_RecordingRow();
352
431
  init_format();
353
- RECENT_LIMIT = 6;
354
432
  }
355
433
  });
356
434
 
357
- // src/tui/RecordingsView.tsx
435
+ // src/tui/RecordingPeek.tsx
358
436
  import { Box as Box6, Text as Text6 } from "ink";
359
- import { jsx as jsx6 } from "react/jsx-runtime";
360
- function RecordingsView({
361
- items,
362
- selectedIndex,
437
+ import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
438
+ function RecordingPeek({
439
+ item,
440
+ summary,
441
+ nowMs,
442
+ width
443
+ }) {
444
+ return /* @__PURE__ */ jsx6(Box6, { width, borderStyle: "round", borderColor: "gray", paddingX: 1, flexDirection: "column", children: !item ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No selection" }) : /* @__PURE__ */ jsx6(PeekBody, { item, summary, nowMs }) });
445
+ }
446
+ function PeekBody({
447
+ item,
448
+ summary,
363
449
  nowMs
364
450
  }) {
365
- if (items.length === 0) {
366
- return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No recordings yet \u2014 run: recappi upload <file>" }) });
367
- }
368
- return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: items.map((item, index) => /* @__PURE__ */ jsx6(
369
- RecordingRow,
370
- {
371
- item,
372
- selected: index === selectedIndex,
373
- nowMs
374
- },
375
- item.recordingId
376
- )) });
451
+ const style = recordingStatusStyle(item.status);
452
+ const meta3 = [
453
+ item.durationMs ? formatClockMs(item.durationMs) : null,
454
+ formatBytes2(item.sizeBytes) || null,
455
+ item.contentType || null
456
+ ].filter(Boolean).join(" \xB7 ");
457
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
458
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "magenta", wrap: "truncate-end", children: recordingTitle2(item) }),
459
+ /* @__PURE__ */ jsx6(Text6, { color: style.color, children: `${style.glyph} ${style.label}` }),
460
+ meta3 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: meta3 }) : null,
461
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: formatAge(item.createdAt, nowMs) }),
462
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx6(SummarySection, { item, summary }) }),
463
+ /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u23CE open \xB7 t transcript \xB7 o web" }) })
464
+ ] });
377
465
  }
378
- var init_RecordingsView = __esm({
379
- "src/tui/RecordingsView.tsx"() {
466
+ function SummarySection({
467
+ item,
468
+ summary
469
+ }) {
470
+ if (!item.activeTranscriptId) return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No transcript yet" });
471
+ if (summary === "loading" || summary === void 0) return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading summary\u2026" });
472
+ if (summary === "error") return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "(summary unavailable)" });
473
+ if (summary.status !== "succeeded" || !summary.tldr) {
474
+ return /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: `Summary ${summary.status}` });
475
+ }
476
+ const points = (summary.keyPoints ?? []).slice(0, 3);
477
+ return /* @__PURE__ */ jsxs5(Fragment, { children: [
478
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Summary" }),
479
+ /* @__PURE__ */ jsx6(Text6, { children: summary.tldr }),
480
+ points.length > 0 ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: points.map((point, i) => /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate-end", children: `\u2022 ${point}` }, i)) }) : null
481
+ ] });
482
+ }
483
+ var init_RecordingPeek = __esm({
484
+ "src/tui/RecordingPeek.tsx"() {
380
485
  "use strict";
486
+ init_format();
381
487
  init_RecordingRow();
382
488
  }
383
489
  });
384
490
 
385
- // src/tui/JobDetailView.tsx
491
+ // src/tui/OverviewView.tsx
386
492
  import { Box as Box7, Text as Text7 } from "ink";
387
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
493
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
494
+ function OverviewView({
495
+ recordings,
496
+ jobs,
497
+ stats,
498
+ selectedIndex,
499
+ spinnerFrame,
500
+ nowMs,
501
+ columns,
502
+ jobStatusByRecording,
503
+ peekItem,
504
+ peekSummary,
505
+ showPeek = false,
506
+ peekWidth = 0
507
+ }) {
508
+ const jobCounts = countJobs(jobs);
509
+ const running = stats?.jobs.running ?? jobCounts.running;
510
+ const queued = stats?.jobs.queued ?? jobCounts.queued;
511
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
512
+ /* @__PURE__ */ jsxs6(Box7, { children: [
513
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Recordings " }),
514
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: stats?.recordings.total ?? recordings.length }),
515
+ stats?.recordings.ready != null ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${stats.recordings.ready} ready` }) : null,
516
+ stats?.recordings.totalDurationMs != null ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${formatClockMs(stats.recordings.totalDurationMs)} transcribed` }) : null,
517
+ running > 0 ? /* @__PURE__ */ jsx7(Text7, { color: "cyan", children: ` \xB7 ${running} transcribing` }) : null,
518
+ queued > 0 ? /* @__PURE__ */ jsx7(Text7, { color: "yellow", children: ` \xB7 ${queued} queued` }) : null
519
+ ] }),
520
+ /* @__PURE__ */ jsxs6(Box7, { flexDirection: "row", alignItems: "flex-start", children: [
521
+ /* @__PURE__ */ jsx7(Box7, { flexGrow: 1, flexDirection: "column", children: /* @__PURE__ */ jsx7(
522
+ RecordingsView,
523
+ {
524
+ items: recordings,
525
+ selectedIndex,
526
+ nowMs,
527
+ columns,
528
+ jobStatusByRecording,
529
+ spinnerFrame
530
+ }
531
+ ) }),
532
+ showPeek ? /* @__PURE__ */ jsx7(Box7, { marginLeft: 1, marginTop: 1, children: /* @__PURE__ */ jsx7(RecordingPeek, { item: peekItem, summary: peekSummary, nowMs, width: peekWidth }) }) : null
533
+ ] })
534
+ ] });
535
+ }
536
+ var init_OverviewView = __esm({
537
+ "src/tui/OverviewView.tsx"() {
538
+ "use strict";
539
+ init_RecordingsView();
540
+ init_RecordingPeek();
541
+ init_format();
542
+ }
543
+ });
544
+
545
+ // src/tui/JobDetailView.tsx
546
+ import { Box as Box8, Text as Text8 } from "ink";
547
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
388
548
  function JobDetailView({
389
549
  item,
390
550
  origin,
@@ -394,13 +554,13 @@ function JobDetailView({
394
554
  const style = statusStyle(item.status);
395
555
  const links = resolveJobLinks(item, origin);
396
556
  const title = item.recording?.title ?? item.recordingId;
397
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", paddingX: 1, children: [
398
- /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
557
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 1, children: [
558
+ /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
399
559
  "\u2039 Jobs / ",
400
560
  title
401
561
  ] }),
402
- /* @__PURE__ */ jsxs5(
403
- Box7,
562
+ /* @__PURE__ */ jsxs7(
563
+ Box8,
404
564
  {
405
565
  marginTop: 1,
406
566
  borderStyle: "round",
@@ -408,17 +568,17 @@ function JobDetailView({
408
568
  paddingX: 1,
409
569
  flexDirection: "column",
410
570
  children: [
411
- /* @__PURE__ */ jsxs5(Text7, { color: style.color, bold: true, children: [
571
+ /* @__PURE__ */ jsxs7(Text8, { color: style.color, bold: true, children: [
412
572
  style.label,
413
- item.provider ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${item.provider}` }) : null
573
+ item.provider ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${item.provider}` }) : null
414
574
  ] }),
415
- /* @__PURE__ */ jsx7(StatusLine, { item, spinnerFrame, nowMs })
575
+ /* @__PURE__ */ jsx8(StatusLine, { item, spinnerFrame, nowMs })
416
576
  ]
417
577
  }
418
578
  ),
419
- /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "column", children: [
420
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Timeline" }),
421
- /* @__PURE__ */ jsx7(
579
+ /* @__PURE__ */ jsxs7(Box8, { marginTop: 1, flexDirection: "column", children: [
580
+ /* @__PURE__ */ jsx8(Text8, { bold: true, children: "Timeline" }),
581
+ /* @__PURE__ */ jsx8(
422
582
  TimelineRow,
423
583
  {
424
584
  label: "Enqueued",
@@ -427,7 +587,7 @@ function JobDetailView({
427
587
  nowMs
428
588
  }
429
589
  ),
430
- /* @__PURE__ */ jsx7(
590
+ /* @__PURE__ */ jsx8(
431
591
  TimelineRow,
432
592
  {
433
593
  label: "Started",
@@ -436,7 +596,7 @@ function JobDetailView({
436
596
  nowMs
437
597
  }
438
598
  ),
439
- /* @__PURE__ */ jsx7(
599
+ /* @__PURE__ */ jsx8(
440
600
  TimelineRow,
441
601
  {
442
602
  label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
@@ -448,21 +608,21 @@ function JobDetailView({
448
608
  }
449
609
  )
450
610
  ] }),
451
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
452
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Recording " }),
611
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
612
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Recording " }),
453
613
  title,
454
- item.recording?.durationMs ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
614
+ item.recording?.durationMs ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
455
615
  ] }) }),
456
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
457
- /* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
458
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
459
- /* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
460
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
461
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "m mac app (soon)" }),
462
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: " \xB7 " }),
463
- /* @__PURE__ */ jsx7(Text7, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
616
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs7(Text8, { children: [
617
+ /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
618
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
619
+ /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
620
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
621
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "m mac app (soon)" }),
622
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
623
+ /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
464
624
  ] }) }),
465
- /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
625
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
466
626
  "esc back \xB7 t transcript",
467
627
  item.transcriptId ? "" : " (when ready)",
468
628
  " \xB7 q quit"
@@ -479,24 +639,24 @@ function StatusLine({
479
639
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
480
640
  if (fraction != null) {
481
641
  const pct = Math.round(fraction * 100);
482
- return /* @__PURE__ */ jsxs5(Text7, { children: [
642
+ return /* @__PURE__ */ jsxs7(Text8, { children: [
483
643
  `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
484
644
  item.recording?.durationMs
485
645
  )}`,
486
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
646
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: elapsed })
487
647
  ] });
488
648
  }
489
649
  const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
490
- return /* @__PURE__ */ jsxs5(Text7, { children: [
650
+ return /* @__PURE__ */ jsxs7(Text8, { children: [
491
651
  `${spinner} transcribing\u2026`,
492
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
652
+ /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: elapsed })
493
653
  ] });
494
654
  }
495
655
  if (item.status === "succeeded")
496
- return /* @__PURE__ */ jsx7(Text7, { children: item.transcriptId ? "transcript ready" : "done" });
497
- if (item.status === "queued") return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "waiting to start\u2026" });
498
- if (item.status === "failed") return /* @__PURE__ */ jsx7(Text7, { color: "red", children: "transcription failed" });
499
- return /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: item.status });
656
+ return /* @__PURE__ */ jsx8(Text8, { children: item.transcriptId ? "transcript ready" : "done" });
657
+ if (item.status === "queued") return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "waiting to start\u2026" });
658
+ if (item.status === "failed") return /* @__PURE__ */ jsx8(Text8, { color: "red", children: "transcription failed" });
659
+ return /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: item.status });
500
660
  }
501
661
  function TimelineRow({
502
662
  label,
@@ -509,10 +669,10 @@ function TimelineRow({
509
669
  const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
510
670
  const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
511
671
  const age = at ? formatAge(at, nowMs) : running ? "now" : "";
512
- return /* @__PURE__ */ jsxs5(Box7, { children: [
513
- /* @__PURE__ */ jsx7(Text7, { color, children: ` ${glyph} ` }),
514
- /* @__PURE__ */ jsx7(Text7, { dimColor: !done && !running, children: label }),
515
- age ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${age}` }) : null
672
+ return /* @__PURE__ */ jsxs7(Box8, { children: [
673
+ /* @__PURE__ */ jsx8(Text8, { color, children: ` ${glyph} ` }),
674
+ /* @__PURE__ */ jsx8(Text8, { dimColor: !done && !running, children: label }),
675
+ age ? /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` ${age}` }) : null
516
676
  ] });
517
677
  }
518
678
  var init_JobDetailView = __esm({
@@ -523,11 +683,13 @@ var init_JobDetailView = __esm({
523
683
  });
524
684
 
525
685
  // src/tui/RecordingDetailView.tsx
526
- import { Box as Box8, Text as Text8 } from "ink";
527
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
686
+ import { Box as Box9, Text as Text9 } from "ink";
687
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
528
688
  function RecordingDetailView({
529
689
  item,
530
- nowMs
690
+ nowMs,
691
+ transcript,
692
+ audio
531
693
  }) {
532
694
  const style = recordingStatusStyle(item.status);
533
695
  const links = resolveRecordingLinks(item.recordingId, item.origin);
@@ -537,96 +699,148 @@ function RecordingDetailView({
537
699
  formatBytes2(item.sizeBytes) || void 0,
538
700
  item.contentType || void 0
539
701
  ].filter(Boolean).join(" \xB7 ");
540
- return /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", paddingX: 1, children: [
541
- /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
542
- "\u2039 Recordings / ",
543
- title
702
+ const summary = typeof transcript === "object" ? transcript.summary : void 0;
703
+ const segments = typeof transcript === "object" ? transcript.segments : void 0;
704
+ return /* @__PURE__ */ jsxs8(Box9, { flexDirection: "column", paddingX: 1, children: [
705
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "\u2039 Recordings" }),
706
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { bold: true, color: "magenta", children: title }) }),
707
+ /* @__PURE__ */ jsxs8(Text9, { children: [
708
+ /* @__PURE__ */ jsx9(Text9, { color: style.color, children: `${style.glyph} ${style.label}` }),
709
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: ` ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
544
710
  ] }),
545
- /* @__PURE__ */ jsxs6(
546
- Box8,
547
- {
548
- marginTop: 1,
549
- borderStyle: "round",
550
- borderColor: style.color,
551
- paddingX: 1,
552
- flexDirection: "column",
553
- children: [
554
- /* @__PURE__ */ jsxs6(Text8, { color: style.color, bold: true, children: [
555
- style.label,
556
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: ` created ${formatAge(item.createdAt, nowMs) || "\u2014"}` })
557
- ] }),
558
- meta3 ? /* @__PURE__ */ jsx8(Text8, { children: meta3 }) : null
559
- ]
560
- }
561
- ),
562
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
563
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Transcript " }),
564
- item.activeTranscriptId ? /* @__PURE__ */ jsx8(Text8, { color: "green", children: "ready \u2014 press t to view" }) : /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "not available yet" })
565
- ] }) }),
566
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs6(Text8, { children: [
567
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
568
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
569
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
570
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
571
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "m mac app (soon)" }),
572
- /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: " \xB7 " }),
573
- /* @__PURE__ */ jsx8(Text8, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
574
- ] }) }),
575
- /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text8, { dimColor: true, children: [
576
- "esc back \xB7 ",
577
- item.activeTranscriptId ? "t transcript \xB7 " : "",
578
- "q quit"
711
+ meta3 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: meta3 }) : null,
712
+ /* @__PURE__ */ jsx9(AudioActionRow, { item, audio }),
713
+ /* @__PURE__ */ jsx9(SummaryCard, { summary, transcript, hasTranscript: !!item.activeTranscriptId }),
714
+ /* @__PURE__ */ jsx9(TranscriptPreview, { segments, transcript, hasTranscript: !!item.activeTranscriptId }),
715
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text9, { dimColor: true, children: [
716
+ `esc back \xB7 o open \xB7 d download \xB7 f finder`,
717
+ item.activeTranscriptId ? " \xB7 t transcript" : "",
718
+ links.webUrl ? " \xB7 w web" : "",
719
+ " \xB7 q quit"
579
720
  ] }) })
580
721
  ] });
581
722
  }
723
+ function AudioActionRow({
724
+ item,
725
+ audio
726
+ }) {
727
+ const ready = item.status === "ready";
728
+ const status = audio?.status ?? "idle";
729
+ let line;
730
+ if (!ready) {
731
+ line = /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Audio available once the recording is ready" });
732
+ } else if (status === "downloading") {
733
+ line = /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Downloading audio\u2026" });
734
+ } else if (status === "opening") {
735
+ line = /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "Opening\u2026" });
736
+ } else if (status === "error") {
737
+ line = /* @__PURE__ */ jsx9(Text9, { color: "red", children: audio?.error ? `Audio failed: ${audio.error}` : "Audio failed" });
738
+ } else if (status === "ready" && audio?.localPath) {
739
+ line = /* @__PURE__ */ jsxs8(Text9, { children: [
740
+ /* @__PURE__ */ jsx9(Text9, { color: "green", children: "\u2713 Downloaded " }),
741
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, wrap: "truncate-middle", children: audio.localPath })
742
+ ] });
743
+ } else {
744
+ line = /* @__PURE__ */ jsxs8(Text9, { children: [
745
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "o" }),
746
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " open in player \xB7 " }),
747
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "d" }),
748
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " download \xB7 " }),
749
+ /* @__PURE__ */ jsx9(Text9, { color: "cyan", children: "f" }),
750
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: " reveal in Finder" })
751
+ ] });
752
+ }
753
+ return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: [
754
+ /* @__PURE__ */ jsx9(Text9, { color: ready ? "cyan" : "gray", children: "\u266A " }),
755
+ line
756
+ ] });
757
+ }
758
+ function SummaryCard({
759
+ summary,
760
+ transcript,
761
+ hasTranscript
762
+ }) {
763
+ if (!hasTranscript) return null;
764
+ return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
765
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Summary" }),
766
+ transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Loading\u2026" }) : transcript === "error" ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "(summary unavailable)" }) : summary && summary.status === "succeeded" && summary.tldr ? /* @__PURE__ */ jsxs8(Fragment2, { children: [
767
+ /* @__PURE__ */ jsx9(Text9, { children: summary.tldr }),
768
+ (summary.keyPoints ?? []).slice(0, 5).map((point, i) => /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `\u2022 ${point}` }, i))
769
+ ] }) : /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `Summary ${summary?.status ?? "unavailable"}` })
770
+ ] });
771
+ }
772
+ function TranscriptPreview({
773
+ segments,
774
+ transcript,
775
+ hasTranscript
776
+ }) {
777
+ if (!hasTranscript) {
778
+ return /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Transcript not available yet" }) });
779
+ }
780
+ if (transcript === "loading" || transcript === void 0 || transcript === "error") {
781
+ return null;
782
+ }
783
+ const shown = (segments ?? []).slice(0, PREVIEW_SEGMENTS);
784
+ return /* @__PURE__ */ jsxs8(Box9, { marginTop: 1, flexDirection: "column", children: [
785
+ /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Transcript" }),
786
+ shown.length === 0 ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "(no segments)" }) : shown.map((seg, i) => /* @__PURE__ */ jsxs8(Text9, { wrap: "truncate-end", children: [
787
+ /* @__PURE__ */ jsx9(Text9, { color: "blue", children: `[${formatClockMs(seg.startMs)}] ` }),
788
+ seg.speaker ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: `${seg.speaker} ` }) : null,
789
+ /* @__PURE__ */ jsx9(Text9, { children: seg.text })
790
+ ] }, i)),
791
+ (segments?.length ?? 0) > PREVIEW_SEGMENTS ? /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: ` \u2026${(segments?.length ?? 0) - PREVIEW_SEGMENTS} more \u2014 press t for full transcript` }) : null
792
+ ] });
793
+ }
794
+ var PREVIEW_SEGMENTS;
582
795
  var init_RecordingDetailView = __esm({
583
796
  "src/tui/RecordingDetailView.tsx"() {
584
797
  "use strict";
585
798
  init_format();
586
799
  init_RecordingRow();
800
+ PREVIEW_SEGMENTS = 6;
587
801
  }
588
802
  });
589
803
 
590
804
  // src/tui/TranscriptView.tsx
591
- import { Box as Box9, Text as Text9 } from "ink";
592
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
805
+ import { Box as Box10, Text as Text10 } from "ink";
806
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
593
807
  function TranscriptView({ loading, data, error: error51 }) {
594
808
  if (loading) {
595
- return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Loading transcript\u2026" }) });
809
+ return /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Loading transcript\u2026" }) });
596
810
  }
597
811
  if (error51) {
598
- return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
599
- /* @__PURE__ */ jsxs7(Text9, { color: "red", children: [
812
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
813
+ /* @__PURE__ */ jsxs9(Text10, { color: "red", children: [
600
814
  "! ",
601
815
  error51
602
816
  ] }),
603
- /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" })
817
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "q / esc / \u2190 back" })
604
818
  ] });
605
819
  }
606
820
  if (!data) {
607
- return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "No transcript." }) });
821
+ return /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "No transcript." }) });
608
822
  }
609
823
  const summary = data.summary;
610
824
  const showSummary = summary?.status === "succeeded";
611
- return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
612
- /* @__PURE__ */ jsx9(Text9, { bold: true, color: "magenta", children: summary?.title ?? "Transcript" }),
613
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, flexDirection: "column", children: data.segments.length === 0 ? /* @__PURE__ */ jsx9(Text9, { children: data.text }) : data.segments.slice(0, 200).map((segment, index) => /* @__PURE__ */ jsxs7(Text9, { children: [
614
- /* @__PURE__ */ jsxs7(Text9, { dimColor: true, children: [
825
+ return /* @__PURE__ */ jsxs9(Box10, { flexDirection: "column", paddingX: 1, children: [
826
+ /* @__PURE__ */ jsx10(Text10, { bold: true, color: "magenta", children: summary?.title ?? "Transcript" }),
827
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, flexDirection: "column", children: data.segments.length === 0 ? /* @__PURE__ */ jsx10(Text10, { children: data.text }) : data.segments.slice(0, 200).map((segment, index) => /* @__PURE__ */ jsxs9(Text10, { children: [
828
+ /* @__PURE__ */ jsxs9(Text10, { dimColor: true, children: [
615
829
  "[",
616
830
  formatClockMs(segment.startMs),
617
831
  "] "
618
832
  ] }),
619
- segment.speaker ? /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
833
+ segment.speaker ? /* @__PURE__ */ jsxs9(Text10, { color: "cyan", children: [
620
834
  segment.speaker,
621
835
  ": "
622
836
  ] }) : null,
623
837
  segment.text
624
838
  ] }, index)) }),
625
- showSummary && summary?.tldr ? /* @__PURE__ */ jsxs7(Box9, { marginTop: 1, flexDirection: "column", children: [
626
- /* @__PURE__ */ jsx9(Text9, { bold: true, children: "Summary" }),
627
- /* @__PURE__ */ jsx9(Text9, { children: summary.tldr })
839
+ showSummary && summary?.tldr ? /* @__PURE__ */ jsxs9(Box10, { marginTop: 1, flexDirection: "column", children: [
840
+ /* @__PURE__ */ jsx10(Text10, { bold: true, children: "Summary" }),
841
+ /* @__PURE__ */ jsx10(Text10, { children: summary.tldr })
628
842
  ] }) : null,
629
- /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" }) })
843
+ /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "q / esc / \u2190 back" }) })
630
844
  ] });
631
845
  }
632
846
  var init_TranscriptView = __esm({
@@ -636,15 +850,27 @@ var init_TranscriptView = __esm({
636
850
  }
637
851
  });
638
852
 
853
+ // src/tui/terminal.ts
854
+ import { useWindowSize } from "ink";
855
+ function useTerminalSize() {
856
+ return useWindowSize();
857
+ }
858
+ var init_terminal = __esm({
859
+ "src/tui/terminal.ts"() {
860
+ "use strict";
861
+ }
862
+ });
863
+
639
864
  // src/tui/AppShell.tsx
640
865
  import { useCallback, useEffect, useState } from "react";
641
- import { Box as Box10, Text as Text10, useApp, useInput } from "ink";
642
- import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
866
+ import { Box as Box11, Text as Text11, useApp, useInput } from "ink";
867
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
643
868
  function AppShell({
644
869
  fetchJobs,
645
870
  fetchTranscript,
646
871
  fetchRecordings,
647
872
  fetchDashboardStats,
873
+ recordingAudio,
648
874
  initialView = "overview",
649
875
  openUrl: openUrl2,
650
876
  copyText: copyText2,
@@ -653,20 +879,48 @@ function AppShell({
653
879
  spinnerMs = 80
654
880
  }) {
655
881
  const { exit } = useApp();
882
+ const size = useTerminalSize();
656
883
  const [jobs, setJobs] = useState([]);
657
884
  const [recordings, setRecordings] = useState([]);
885
+ const [recordingsNextCursor, setRecordingsNextCursor] = useState(null);
886
+ const [recordingsTotalCount, setRecordingsTotalCount] = useState(void 0);
658
887
  const [stats, setStats] = useState(void 0);
659
888
  const [origin, setOrigin] = useState("");
660
889
  const [stack, setStack] = useState([{ kind: initialView }]);
661
890
  const [selected, setSelected] = useState(0);
662
891
  const [spinnerFrame, setSpinnerFrame] = useState(0);
892
+ const [loadingMoreRecordings, setLoadingMoreRecordings] = useState(false);
663
893
  const [loadError, setLoadError] = useState(void 0);
664
894
  const [notice, setNotice] = useState(void 0);
895
+ const [summaryCache, setSummaryCache] = useState(() => /* @__PURE__ */ new Map());
896
+ const [transcriptCache, setTranscriptCache] = useState(
897
+ () => /* @__PURE__ */ new Map()
898
+ );
899
+ const [audioCache, setAudioCache] = useState(() => /* @__PURE__ */ new Map());
665
900
  const screen = stack[stack.length - 1];
666
- const refresh = useCallback(async () => {
901
+ const selectedRecording = screen.kind === "overview" ? recordings[selected] : void 0;
902
+ const peekTranscriptId = selectedRecording?.activeTranscriptId ?? void 0;
903
+ useEffect(() => {
904
+ if (!peekTranscriptId || summaryCache.has(peekTranscriptId)) return;
905
+ let cancelled = false;
906
+ const timer = setTimeout(() => {
907
+ setSummaryCache((m) => new Map(m).set(peekTranscriptId, "loading"));
908
+ fetchTranscript(peekTranscriptId).then((tr) => {
909
+ if (!cancelled) setSummaryCache((m) => new Map(m).set(peekTranscriptId, tr.summary));
910
+ }).catch(() => {
911
+ if (!cancelled) setSummaryCache((m) => new Map(m).set(peekTranscriptId, "error"));
912
+ });
913
+ }, 200);
914
+ return () => {
915
+ cancelled = true;
916
+ clearTimeout(timer);
917
+ };
918
+ }, [peekTranscriptId, fetchTranscript]);
919
+ const peekSummary = peekTranscriptId ? summaryCache.get(peekTranscriptId) : void 0;
920
+ const refresh = useCallback(async ({ resetRecordings = false } = {}) => {
667
921
  const [jobsR, recR, statsR] = await Promise.allSettled([
668
922
  fetchJobs(),
669
- fetchRecordings ? fetchRecordings() : Promise.resolve(void 0),
923
+ resetRecordings && fetchRecordings ? fetchRecordings({ limit: RECORDINGS_PAGE_SIZE }) : Promise.resolve(void 0),
670
924
  fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0)
671
925
  ]);
672
926
  if (jobsR.status === "fulfilled") {
@@ -676,11 +930,40 @@ function AppShell({
676
930
  } else {
677
931
  setLoadError(jobsR.reason instanceof Error ? jobsR.reason.message : String(jobsR.reason));
678
932
  }
679
- if (recR.status === "fulfilled" && recR.value) setRecordings(recR.value.items);
933
+ if (recR.status === "fulfilled" && recR.value) {
934
+ setRecordings(recR.value.items);
935
+ setRecordingsNextCursor(recR.value.nextCursor ?? null);
936
+ setRecordingsTotalCount(recR.value.totalCount);
937
+ }
680
938
  if (statsR.status === "fulfilled" && statsR.value) setStats(statsR.value);
681
939
  }, [fetchJobs, fetchRecordings, fetchDashboardStats]);
940
+ const loadMoreRecordings = useCallback(async () => {
941
+ if (!fetchRecordings || !recordingsNextCursor || loadingMoreRecordings) return;
942
+ setLoadingMoreRecordings(true);
943
+ try {
944
+ const page = await fetchRecordings({
945
+ limit: RECORDINGS_PAGE_SIZE,
946
+ cursor: recordingsNextCursor
947
+ });
948
+ setRecordings((prev) => {
949
+ const seen = new Set(prev.map((item) => item.recordingId));
950
+ const merged = [...prev];
951
+ for (const item of page.items) {
952
+ if (!seen.has(item.recordingId)) merged.push(item);
953
+ }
954
+ return merged;
955
+ });
956
+ setRecordingsNextCursor(page.nextCursor ?? null);
957
+ setRecordingsTotalCount(page.totalCount);
958
+ setLoadError(void 0);
959
+ } catch (error51) {
960
+ setLoadError(error51 instanceof Error ? error51.message : String(error51));
961
+ } finally {
962
+ setLoadingMoreRecordings(false);
963
+ }
964
+ }, [fetchRecordings, loadingMoreRecordings, recordingsNextCursor]);
682
965
  useEffect(() => {
683
- void refresh();
966
+ void refresh({ resetRecordings: true });
684
967
  const id = setInterval(() => void refresh(), pollMs);
685
968
  return () => clearInterval(id);
686
969
  }, [refresh, pollMs]);
@@ -690,11 +973,32 @@ function AppShell({
690
973
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
691
974
  return () => clearInterval(id);
692
975
  }, [hasRunning, spinnerMs]);
693
- const recordingList = screen.kind === "overview" ? overviewRecentRecordings(recordings) : recordings;
694
- const listLength = screen.kind === "jobs" ? jobs.length : recordingList.length;
976
+ const jobRank = (s) => s === "running" ? 4 : s === "queued" ? 3 : s === "failed" ? 2 : s === "succeeded" ? 1 : 0;
977
+ const jobStatusByRecording = /* @__PURE__ */ new Map();
978
+ for (const job of jobs) {
979
+ const prev = jobStatusByRecording.get(job.recordingId);
980
+ if (!prev || jobRank(job.status) > jobRank(prev)) {
981
+ jobStatusByRecording.set(job.recordingId, job.status);
982
+ }
983
+ }
984
+ const listLength = screen.kind === "jobs" ? jobs.length : recordings.length;
695
985
  useEffect(() => {
696
986
  setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
697
987
  }, [listLength]);
988
+ const visibleRecordingRows = Math.max(3, size.rows - 6);
989
+ useEffect(() => {
990
+ if (screen.kind !== "overview" || !recordingsNextCursor) return;
991
+ const nearLoadedEnd = recordings.length - selected <= RECORDINGS_PREFETCH_REMAINING;
992
+ const underfilledViewport = recordings.length < visibleRecordingRows;
993
+ if (nearLoadedEnd || underfilledViewport) void loadMoreRecordings();
994
+ }, [
995
+ loadMoreRecordings,
996
+ recordings.length,
997
+ recordingsNextCursor,
998
+ screen.kind,
999
+ selected,
1000
+ visibleRecordingRows
1001
+ ]);
698
1002
  const openTranscript = useCallback(
699
1003
  async (transcriptId) => {
700
1004
  setStack((st) => [...st, { kind: "transcript", loading: true }]);
@@ -714,6 +1018,47 @@ function AppShell({
714
1018
  },
715
1019
  [fetchTranscript]
716
1020
  );
1021
+ const detailTranscriptId = screen.kind === "recordingDetail" ? recordings.find((r) => r.recordingId === screen.recordingId)?.activeTranscriptId : void 0;
1022
+ useEffect(() => {
1023
+ if (!detailTranscriptId || transcriptCache.has(detailTranscriptId)) return;
1024
+ let cancelled = false;
1025
+ setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "loading"));
1026
+ fetchTranscript(detailTranscriptId).then((tr) => {
1027
+ if (!cancelled) setTranscriptCache((m) => new Map(m).set(detailTranscriptId, tr));
1028
+ }).catch(() => {
1029
+ if (!cancelled) setTranscriptCache((m) => new Map(m).set(detailTranscriptId, "error"));
1030
+ });
1031
+ return () => {
1032
+ cancelled = true;
1033
+ };
1034
+ }, [detailTranscriptId, fetchTranscript]);
1035
+ const setAudio = (recordingId, action) => setAudioCache((m) => new Map(m).set(recordingId, action));
1036
+ const runAudio = useCallback(
1037
+ async (recordingId, mode) => {
1038
+ if (!recordingAudio) {
1039
+ setNotice("Audio actions are not available");
1040
+ return;
1041
+ }
1042
+ setAudio(recordingId, { status: "downloading" });
1043
+ try {
1044
+ const localPath = await recordingAudio.downloadRecordingAudio(recordingId);
1045
+ if (mode === "open") {
1046
+ setAudio(recordingId, { status: "opening", localPath });
1047
+ await recordingAudio.openPath(localPath);
1048
+ } else if (mode === "finder") {
1049
+ setAudio(recordingId, { status: "opening", localPath });
1050
+ await recordingAudio.revealInFinder(localPath);
1051
+ }
1052
+ setAudio(recordingId, { status: "ready", localPath });
1053
+ } catch (error51) {
1054
+ setAudio(recordingId, {
1055
+ status: "error",
1056
+ error: error51 instanceof Error ? error51.message : String(error51)
1057
+ });
1058
+ }
1059
+ },
1060
+ [recordingAudio]
1061
+ );
717
1062
  const goTab = (tab2) => {
718
1063
  setStack([{ kind: tab2 }]);
719
1064
  setSelected(0);
@@ -726,13 +1071,11 @@ function AppShell({
726
1071
  if (key.escape || key.leftArrow) return back();
727
1072
  if (input === "1") return goTab("overview");
728
1073
  if (input === "2") return goTab("jobs");
729
- if (input === "3") return goTab("recordings");
730
- if (input === "r") return void refresh();
731
- if (screen.kind === "overview" || screen.kind === "recordings") {
1074
+ if (input === "r") return void refresh({ resetRecordings: true });
1075
+ if (screen.kind === "overview") {
732
1076
  if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
733
- if (key.downArrow || input === "j")
734
- setSelected((i) => Math.min(recordingList.length - 1, i + 1));
735
- const rec = recordingList[selected];
1077
+ if (key.downArrow || input === "j") setSelected((i) => Math.min(recordings.length - 1, i + 1));
1078
+ const rec = recordings[selected];
736
1079
  if (key.return && rec)
737
1080
  setStack((st) => [...st, { kind: "recordingDetail", recordingId: rec.recordingId }]);
738
1081
  if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
@@ -762,8 +1105,10 @@ function AppShell({
762
1105
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
763
1106
  const links = rec ? resolveRecordingLinks(rec.recordingId, rec.origin) : {};
764
1107
  if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
765
- else if ((input === "o" || input === "w") && links.webUrl) openUrl2?.(links.webUrl);
766
- else if (input === "m") setNotice("Mac app deeplink not available yet");
1108
+ else if (input === "o" && rec) void runAudio(rec.recordingId, "open");
1109
+ else if (input === "d" && rec) void runAudio(rec.recordingId, "download");
1110
+ else if (input === "f" && rec) void runAudio(rec.recordingId, "finder");
1111
+ else if (input === "w" && links.webUrl) openUrl2?.(links.webUrl);
767
1112
  else if (input === "c" && links.webUrl) {
768
1113
  copyText2?.(links.webUrl);
769
1114
  setNotice("Link copied");
@@ -772,69 +1117,116 @@ function AppShell({
772
1117
  }
773
1118
  });
774
1119
  if (screen.kind === "transcript") {
775
- return /* @__PURE__ */ jsx10(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
1120
+ return /* @__PURE__ */ jsx11(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
776
1121
  }
777
1122
  if (screen.kind === "jobDetail") {
778
1123
  const job = jobs.find((j) => j.jobId === screen.jobId);
779
- if (!job) return /* @__PURE__ */ jsx10(Missing, { label: "Job" });
780
- return /* @__PURE__ */ jsx10(Detail, { notice, children: /* @__PURE__ */ jsx10(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
1124
+ if (!job) return /* @__PURE__ */ jsx11(Missing, { label: "Job" });
1125
+ return /* @__PURE__ */ jsx11(Detail, { notice, children: /* @__PURE__ */ jsx11(JobDetailView, { item: job, origin, spinnerFrame, nowMs: now() }) });
781
1126
  }
782
1127
  if (screen.kind === "recordingDetail") {
783
1128
  const rec = recordings.find((r) => r.recordingId === screen.recordingId);
784
- if (!rec) return /* @__PURE__ */ jsx10(Missing, { label: "Recording" });
785
- return /* @__PURE__ */ jsx10(Detail, { notice, children: /* @__PURE__ */ jsx10(RecordingDetailView, { item: rec, nowMs: now() }) });
786
- }
787
- const tab = screen.kind === "jobs" ? "jobs" : screen.kind === "recordings" ? "recordings" : "overview";
788
- const footerKeys = screen.kind === "jobs" ? "1/2/3 tabs \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 r refresh \xB7 q quit" : "1/2/3 tabs \xB7 \u2191\u2193 select \xB7 \u23CE recording \xB7 t transcript \xB7 r refresh \xB7 q quit";
789
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
790
- /* @__PURE__ */ jsx10(Header, { active: tab }),
791
- screen.kind === "overview" ? /* @__PURE__ */ jsx10(
1129
+ if (!rec) return /* @__PURE__ */ jsx11(Missing, { label: "Recording" });
1130
+ const detailTranscript = rec.activeTranscriptId ? transcriptCache.get(rec.activeTranscriptId) : void 0;
1131
+ return /* @__PURE__ */ jsx11(Detail, { notice, children: /* @__PURE__ */ jsx11(
1132
+ RecordingDetailView,
1133
+ {
1134
+ item: rec,
1135
+ nowMs: now(),
1136
+ transcript: detailTranscript,
1137
+ audio: audioCache.get(rec.recordingId)
1138
+ }
1139
+ ) });
1140
+ }
1141
+ const tab = screen.kind === "jobs" ? "jobs" : "overview";
1142
+ let body;
1143
+ let position = "";
1144
+ if (screen.kind === "overview") {
1145
+ const listBudget = Math.max(3, size.rows - 6);
1146
+ const buckets = recordings.map((r) => dateBucket(r.createdAt, now()));
1147
+ const win = groupedListWindow(buckets, selected, listBudget);
1148
+ const totalRecordings = Math.max(
1149
+ recordingsTotalCount ?? stats?.recordings.total ?? recordings.length,
1150
+ recordings.length
1151
+ );
1152
+ position = recordings.length ? `${selected + 1} / ${totalRecordings}${loadingMoreRecordings ? " \xB7 loading" : ""}` : loadingMoreRecordings ? "loading" : "0";
1153
+ const showPeek = size.columns >= 100;
1154
+ const peekWidth = showPeek ? 34 : 0;
1155
+ const listColumns = showPeek ? Math.max(30, size.columns - peekWidth - 3) : size.columns;
1156
+ body = /* @__PURE__ */ jsx11(
792
1157
  OverviewView,
793
1158
  {
794
- recordings,
1159
+ recordings: recordings.slice(win.start, win.end),
1160
+ selectedIndex: selected - win.start,
795
1161
  jobs,
796
1162
  stats,
797
- selectedIndex: selected,
1163
+ nowMs: now(),
1164
+ columns: listColumns,
1165
+ jobStatusByRecording,
798
1166
  spinnerFrame,
799
- nowMs: now()
800
- }
801
- ) : screen.kind === "recordings" ? /* @__PURE__ */ jsx10(RecordingsView, { items: recordings, selectedIndex: selected, nowMs: now() }) : /* @__PURE__ */ jsx10(JobsView, { items: jobs, selectedIndex: selected, spinnerFrame }),
802
- loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx10(Box10, { marginTop: 1, children: /* @__PURE__ */ jsxs8(Text10, { color: "red", children: [
803
- "! ",
804
- loadError
805
- ] }) }) : null,
806
- /* @__PURE__ */ jsx10(Footer, { keys: footerKeys })
1167
+ peekItem: recordings[selected],
1168
+ peekSummary,
1169
+ showPeek,
1170
+ peekWidth
1171
+ }
1172
+ );
1173
+ } else {
1174
+ const win = listWindow(selected, jobs.length, Math.max(3, size.rows - 4));
1175
+ position = jobs.length ? `${selected + 1} / ${jobs.length}` : "0";
1176
+ body = /* @__PURE__ */ jsx11(
1177
+ JobsView,
1178
+ {
1179
+ items: jobs.slice(win.start, win.end),
1180
+ selectedIndex: selected - win.start,
1181
+ spinnerFrame
1182
+ }
1183
+ );
1184
+ }
1185
+ 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`;
1186
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", height: size.rows, paddingX: 1, children: [
1187
+ /* @__PURE__ */ jsx11(Header, { active: tab }),
1188
+ /* @__PURE__ */ jsxs10(Box11, { flexGrow: 1, flexDirection: "column", children: [
1189
+ body,
1190
+ loadError && jobs.length === 0 && recordings.length === 0 ? /* @__PURE__ */ jsx11(Box11, { marginTop: 1, children: /* @__PURE__ */ jsxs10(Text11, { color: "red", children: [
1191
+ "! ",
1192
+ loadError
1193
+ ] }) }) : null
1194
+ ] }),
1195
+ /* @__PURE__ */ jsx11(Footer, { keys: footerKeys })
807
1196
  ] });
808
1197
  }
809
1198
  function Detail({
810
1199
  notice,
811
1200
  children
812
1201
  }) {
813
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", children: [
1202
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", children: [
814
1203
  children,
815
- notice ? /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: notice }) }) : null
1204
+ notice ? /* @__PURE__ */ jsx11(Box11, { paddingX: 1, children: /* @__PURE__ */ jsx11(Text11, { color: "green", children: notice }) }) : null
816
1205
  ] });
817
1206
  }
818
1207
  function Missing({ label }) {
819
- return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
820
- /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
1208
+ return /* @__PURE__ */ jsxs10(Box11, { flexDirection: "column", paddingX: 1, children: [
1209
+ /* @__PURE__ */ jsxs10(Text11, { dimColor: true, children: [
821
1210
  label,
822
1211
  " no longer in the list."
823
1212
  ] }),
824
- /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "esc back \xB7 q quit" })
1213
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: "esc back \xB7 q quit" })
825
1214
  ] });
826
1215
  }
1216
+ var RECORDINGS_PAGE_SIZE, RECORDINGS_PREFETCH_REMAINING;
827
1217
  var init_AppShell = __esm({
828
1218
  "src/tui/AppShell.tsx"() {
829
1219
  "use strict";
830
1220
  init_chrome();
831
1221
  init_JobsView();
832
1222
  init_OverviewView();
833
- init_RecordingsView();
834
1223
  init_JobDetailView();
835
1224
  init_RecordingDetailView();
836
1225
  init_TranscriptView();
837
1226
  init_format();
1227
+ init_terminal();
1228
+ RECORDINGS_PAGE_SIZE = 50;
1229
+ RECORDINGS_PREFETCH_REMAINING = 8;
838
1230
  }
839
1231
  });
840
1232
 
@@ -842,43 +1234,49 @@ var init_AppShell = __esm({
842
1234
  var tui_exports = {};
843
1235
  __export(tui_exports, {
844
1236
  AppShell: () => AppShell,
1237
+ DASHBOARD_RENDER_OPTIONS: () => DASHBOARD_RENDER_OPTIONS,
845
1238
  JobDetailView: () => JobDetailView,
846
1239
  JobsView: () => JobsView,
847
1240
  OverviewView: () => OverviewView,
848
- runDashboard: () => runDashboard
1241
+ runDashboard: () => runDashboard,
1242
+ useTerminalSize: () => useTerminalSize
849
1243
  });
850
- import React2 from "react";
1244
+ import React3 from "react";
851
1245
  import { render } from "ink";
852
- import { spawn } from "child_process";
1246
+ import { spawn as spawn2 } from "child_process";
853
1247
  function openUrl(url2) {
854
1248
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
855
1249
  try {
856
- spawn(cmd, [url2], { stdio: "ignore", detached: true }).unref();
1250
+ spawn2(cmd, [url2], { stdio: "ignore", detached: true }).unref();
857
1251
  } catch {
858
1252
  }
859
1253
  }
860
1254
  function copyText(text) {
861
1255
  if (process.platform !== "darwin") return;
862
1256
  try {
863
- const child = spawn("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
1257
+ const child = spawn2("pbcopy", { stdio: ["pipe", "ignore", "ignore"] });
864
1258
  child.stdin.end(text);
865
1259
  } catch {
866
1260
  }
867
1261
  }
868
1262
  async function runDashboard(deps) {
869
- const app = render(
870
- React2.createElement(AppShell, {
1263
+ const renderApp = deps.renderApp ?? render;
1264
+ const app = renderApp(
1265
+ React3.createElement(AppShell, {
871
1266
  fetchJobs: deps.fetchJobs,
872
1267
  fetchTranscript: deps.fetchTranscript,
873
1268
  fetchRecordings: deps.fetchRecordings,
874
1269
  fetchDashboardStats: deps.fetchDashboardStats,
1270
+ recordingAudio: deps.recordingAudio,
875
1271
  initialView: deps.initialView ?? "overview",
876
1272
  openUrl,
877
1273
  copyText
878
- })
1274
+ }),
1275
+ DASHBOARD_RENDER_OPTIONS
879
1276
  );
880
1277
  await app.waitUntilExit();
881
1278
  }
1279
+ var DASHBOARD_RENDER_OPTIONS;
882
1280
  var init_tui = __esm({
883
1281
  "src/tui/index.ts"() {
884
1282
  "use strict";
@@ -887,12 +1285,17 @@ var init_tui = __esm({
887
1285
  init_JobsView();
888
1286
  init_OverviewView();
889
1287
  init_JobDetailView();
1288
+ init_terminal();
1289
+ DASHBOARD_RENDER_OPTIONS = {
1290
+ alternateScreen: true,
1291
+ interactive: true
1292
+ };
890
1293
  }
891
1294
  });
892
1295
 
893
1296
  // src/cli.ts
894
1297
  import { Command, CommanderError, InvalidArgumentError } from "commander/esm.mjs";
895
- import os3 from "os";
1298
+ import os4 from "os";
896
1299
 
897
1300
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
898
1301
  var external_exports = {};
@@ -1660,10 +2063,10 @@ function mergeDefs(...defs) {
1660
2063
  function cloneDef(schema) {
1661
2064
  return mergeDefs(schema._zod.def);
1662
2065
  }
1663
- function getElementAtPath(obj, path3) {
1664
- if (!path3)
2066
+ function getElementAtPath(obj, path4) {
2067
+ if (!path4)
1665
2068
  return obj;
1666
- return path3.reduce((acc, key) => acc?.[key], obj);
2069
+ return path4.reduce((acc, key) => acc?.[key], obj);
1667
2070
  }
1668
2071
  function promiseAllObject(promisesObj) {
1669
2072
  const keys = Object.keys(promisesObj);
@@ -2072,11 +2475,11 @@ function explicitlyAborted(x, startIndex = 0) {
2072
2475
  }
2073
2476
  return false;
2074
2477
  }
2075
- function prefixIssues(path3, issues) {
2478
+ function prefixIssues(path4, issues) {
2076
2479
  return issues.map((iss) => {
2077
2480
  var _a3;
2078
2481
  (_a3 = iss).path ?? (_a3.path = []);
2079
- iss.path.unshift(path3);
2482
+ iss.path.unshift(path4);
2080
2483
  return iss;
2081
2484
  });
2082
2485
  }
@@ -2223,16 +2626,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2223
2626
  }
2224
2627
  function formatError(error51, mapper = (issue2) => issue2.message) {
2225
2628
  const fieldErrors = { _errors: [] };
2226
- const processError = (error52, path3 = []) => {
2629
+ const processError = (error52, path4 = []) => {
2227
2630
  for (const issue2 of error52.issues) {
2228
2631
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2229
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
2632
+ issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
2230
2633
  } else if (issue2.code === "invalid_key") {
2231
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2634
+ processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2232
2635
  } else if (issue2.code === "invalid_element") {
2233
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2636
+ processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2234
2637
  } else {
2235
- const fullpath = [...path3, ...issue2.path];
2638
+ const fullpath = [...path4, ...issue2.path];
2236
2639
  if (fullpath.length === 0) {
2237
2640
  fieldErrors._errors.push(mapper(issue2));
2238
2641
  } else {
@@ -2259,17 +2662,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2259
2662
  }
2260
2663
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2261
2664
  const result = { errors: [] };
2262
- const processError = (error52, path3 = []) => {
2665
+ const processError = (error52, path4 = []) => {
2263
2666
  var _a3, _b;
2264
2667
  for (const issue2 of error52.issues) {
2265
2668
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2266
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
2669
+ issue2.errors.map((issues) => processError({ issues }, [...path4, ...issue2.path]));
2267
2670
  } else if (issue2.code === "invalid_key") {
2268
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2671
+ processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2269
2672
  } else if (issue2.code === "invalid_element") {
2270
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
2673
+ processError({ issues: issue2.issues }, [...path4, ...issue2.path]);
2271
2674
  } else {
2272
- const fullpath = [...path3, ...issue2.path];
2675
+ const fullpath = [...path4, ...issue2.path];
2273
2676
  if (fullpath.length === 0) {
2274
2677
  result.errors.push(mapper(issue2));
2275
2678
  continue;
@@ -2301,8 +2704,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2301
2704
  }
2302
2705
  function toDotPath(_path) {
2303
2706
  const segs = [];
2304
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2305
- for (const seg of path3) {
2707
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2708
+ for (const seg of path4) {
2306
2709
  if (typeof seg === "number")
2307
2710
  segs.push(`[${seg}]`);
2308
2711
  else if (typeof seg === "symbol")
@@ -14994,13 +15397,13 @@ function resolveRef(ref, ctx) {
14994
15397
  if (!ref.startsWith("#")) {
14995
15398
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
14996
15399
  }
14997
- const path3 = ref.slice(1).split("/").filter(Boolean);
14998
- if (path3.length === 0) {
15400
+ const path4 = ref.slice(1).split("/").filter(Boolean);
15401
+ if (path4.length === 0) {
14999
15402
  return ctx.rootSchema;
15000
15403
  }
15001
15404
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15002
- if (path3[0] === defsKey) {
15003
- const key = path3[1];
15405
+ if (path4[0] === defsKey) {
15406
+ const key = path4[1];
15004
15407
  if (!key || !ctx.defs[key]) {
15005
15408
  throw new Error(`Reference not found: ${ref}`);
15006
15409
  }
@@ -16084,7 +16487,11 @@ function isRecord(value) {
16084
16487
  }
16085
16488
 
16086
16489
  // src/api.ts
16087
- import { promises as fs3 } from "fs";
16490
+ import { createWriteStream, promises as fs3 } from "fs";
16491
+ import os3 from "os";
16492
+ import path3 from "path";
16493
+ import { Readable } from "stream";
16494
+ import { pipeline } from "stream/promises";
16088
16495
 
16089
16496
  // src/files.ts
16090
16497
  import { promises as fs2 } from "fs";
@@ -16389,6 +16796,36 @@ var RecappiApiClient = class {
16389
16796
  );
16390
16797
  return mapRecording(parsed, this.auth.origin);
16391
16798
  }
16799
+ async downloadRecordingAudio(recordingId, opts = {}) {
16800
+ const response = await this.request(
16801
+ "GET",
16802
+ `/api/recordings/${encodeURIComponent(recordingId)}/audio`
16803
+ );
16804
+ if (!response.body) {
16805
+ throw cliError("cloud.invalid_response", "Recording audio response was empty.");
16806
+ }
16807
+ const contentType = normalizeContentType(response.headers.get("content-type"));
16808
+ const contentLength = numberHeader(response.headers.get("content-length"));
16809
+ const dir = opts.directory ?? await fs3.mkdtemp(path3.join(os3.tmpdir(), "recappi-cli-audio-"));
16810
+ if (opts.directory) await fs3.mkdir(dir, { recursive: true });
16811
+ const filePath = path3.join(dir, recordingAudioFileName(recordingId, opts.title, contentType));
16812
+ try {
16813
+ await pipeline(
16814
+ Readable.fromWeb(response.body),
16815
+ createWriteStream(filePath)
16816
+ );
16817
+ } catch (error51) {
16818
+ await fs3.rm(filePath, { force: true }).catch(() => void 0);
16819
+ throw error51;
16820
+ }
16821
+ return {
16822
+ recordingId,
16823
+ localPath: filePath,
16824
+ contentType,
16825
+ ...contentLength !== void 0 ? { contentLength } : {},
16826
+ origin: this.auth.origin
16827
+ };
16828
+ }
16392
16829
  async dashboardStats() {
16393
16830
  const parsed = await this.getJson("/api/dashboard/stats");
16394
16831
  return mapDashboardStats(parsed, this.auth.origin);
@@ -16573,12 +17010,12 @@ var RecappiApiClient = class {
16573
17010
  ...typeof parsed.language === "string" || parsed.language === null ? { language: parsed.language } : {}
16574
17011
  };
16575
17012
  }
16576
- async getJson(path3) {
16577
- const response = await this.request("GET", path3);
17013
+ async getJson(path4) {
17014
+ const response = await this.request("GET", path4);
16578
17015
  return await parseJson(response);
16579
17016
  }
16580
- async postJson(path3, body) {
16581
- const response = await this.request("POST", path3, JSON.stringify(body), {
17017
+ async postJson(path4, body) {
17018
+ const response = await this.request("POST", path4, JSON.stringify(body), {
16582
17019
  headers: { "content-type": "application/json" }
16583
17020
  });
16584
17021
  return await parseJson(response);
@@ -16621,6 +17058,53 @@ async function responseMessage(response) {
16621
17058
  return response.statusText;
16622
17059
  }
16623
17060
  }
17061
+ function normalizeContentType(value) {
17062
+ return value?.split(";")[0]?.trim().toLowerCase() || "audio/wav";
17063
+ }
17064
+ function numberHeader(value) {
17065
+ if (!value) return void 0;
17066
+ const parsed = Number(value);
17067
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
17068
+ }
17069
+ function audioExtensionForContentType(contentType) {
17070
+ switch (contentType) {
17071
+ case "audio/mpeg":
17072
+ case "audio/mp3":
17073
+ return "mp3";
17074
+ case "audio/aiff":
17075
+ case "audio/x-aiff":
17076
+ return "aiff";
17077
+ case "audio/aac":
17078
+ case "audio/mp4":
17079
+ case "audio/m4a":
17080
+ case "audio/x-m4a":
17081
+ return "m4a";
17082
+ case "audio/ogg":
17083
+ return "ogg";
17084
+ case "audio/flac":
17085
+ case "audio/x-flac":
17086
+ return "flac";
17087
+ case "audio/webm":
17088
+ return "webm";
17089
+ case "audio/wav":
17090
+ case "audio/x-wav":
17091
+ default:
17092
+ return "wav";
17093
+ }
17094
+ }
17095
+ function recordingAudioFileName(recordingId, title, contentType) {
17096
+ const idStem = truncateFileStem(safeFileStem(recordingId), 48);
17097
+ const titleStem = title ? truncateFileStem(safeFileStem(title), 80) : "";
17098
+ const stem = titleStem ? `${titleStem}-${idStem}` : idStem;
17099
+ return `${stem}.${audioExtensionForContentType(contentType)}`;
17100
+ }
17101
+ function truncateFileStem(value, maxLength) {
17102
+ return [...value].slice(0, maxLength).join("");
17103
+ }
17104
+ function safeFileStem(value) {
17105
+ const safe = value.normalize("NFKC").replace(/[^\p{L}\p{N}._-]+/gu, "-").replace(/^-+|-+$/g, "");
17106
+ return safe || "recording";
17107
+ }
16624
17108
  function isRecord2(value) {
16625
17109
  return typeof value === "object" && value !== null && !Array.isArray(value);
16626
17110
  }
@@ -16858,6 +17342,62 @@ function decodeJsonRecord(value) {
16858
17342
  }
16859
17343
  }
16860
17344
 
17345
+ // src/audio.ts
17346
+ import { spawn } from "child_process";
17347
+ function createRecordingAudioRuntime(client, deps = {}) {
17348
+ return {
17349
+ downloadRecordingAudio: async (recordingId, opts) => (await client.downloadRecordingAudio(recordingId, opts)).localPath,
17350
+ openPath: (localPath) => openPath(localPath, deps),
17351
+ revealInFinder: (localPath) => revealInFinder(localPath, deps)
17352
+ };
17353
+ }
17354
+ function openPath(localPath, deps = {}) {
17355
+ return runMacOpen([localPath], deps);
17356
+ }
17357
+ function revealInFinder(localPath, deps = {}) {
17358
+ return runMacOpen(["-R", localPath], deps);
17359
+ }
17360
+ function runMacOpen(args, deps) {
17361
+ if ((deps.platform ?? process.platform) !== "darwin") {
17362
+ return Promise.reject(
17363
+ cliError(
17364
+ "usage.invalid_argument",
17365
+ "Recording audio file actions are supported on macOS only.",
17366
+ {
17367
+ hint: "Download the audio and open the printed local path manually on this platform."
17368
+ }
17369
+ )
17370
+ );
17371
+ }
17372
+ const spawnProcess = deps.spawnProcess ?? spawn;
17373
+ return new Promise((resolve, reject) => {
17374
+ let settled = false;
17375
+ const finish = (error51) => {
17376
+ if (settled) return;
17377
+ settled = true;
17378
+ if (error51) reject(error51);
17379
+ else resolve();
17380
+ };
17381
+ try {
17382
+ const child = spawnProcess("open", args, { stdio: "ignore" });
17383
+ child.once(
17384
+ "error",
17385
+ (error51) => finish(error51 instanceof Error ? error51 : new Error(String(error51)))
17386
+ );
17387
+ child.once("close", (code) => {
17388
+ if (code === 0) finish();
17389
+ else {
17390
+ finish(
17391
+ cliError("internal.unexpected", `open failed with exit code ${code ?? "unknown"}.`)
17392
+ );
17393
+ }
17394
+ });
17395
+ } catch (error51) {
17396
+ finish(error51 instanceof Error ? error51 : new Error(String(error51)));
17397
+ }
17398
+ });
17399
+ }
17400
+
16861
17401
  // src/render.ts
16862
17402
  function createHumanProgressState(interactive) {
16863
17403
  return {
@@ -17374,11 +17914,11 @@ function buildSchemaDocument(program) {
17374
17914
  event: toJsonSchema(operationEventSchema)
17375
17915
  };
17376
17916
  }
17377
- function walkCommands(command, path3, out) {
17917
+ function walkCommands(command, path4, out) {
17378
17918
  for (const sub of subcommandsOf(command)) {
17379
17919
  const name = sub.name();
17380
17920
  if (name === "help") continue;
17381
- const fullPath = [...path3, name];
17921
+ const fullPath = [...path4, name];
17382
17922
  const children = subcommandsOf(sub).filter((child) => child.name() !== "help");
17383
17923
  if (children.length === 0) {
17384
17924
  out.push(leafCommandDoc(sub, fullPath.join(" ")));
@@ -17438,6 +17978,7 @@ function readCliVersion() {
17438
17978
  var CLI_VERSION = readCliVersion();
17439
17979
 
17440
17980
  // src/cli.ts
17981
+ var DASHBOARD_RECORDINGS_PAGE_SIZE = 50;
17441
17982
  async function runCli(deps = {}) {
17442
17983
  const argv = deps.argv ?? process.argv.slice(2);
17443
17984
  const stdout = deps.stdout ?? ((text) => process.stdout.write(text));
@@ -17481,9 +18022,10 @@ async function runCli(deps = {}) {
17481
18022
  const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
17482
18023
  await runDashboard2({
17483
18024
  fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
17484
- fetchRecordings: () => client.listRecordings({ limit: 20 }),
18025
+ fetchRecordings: ({ cursor, limit = DASHBOARD_RECORDINGS_PAGE_SIZE } = {}) => client.listRecordings({ limit, cursor }),
17485
18026
  fetchDashboardStats: () => client.dashboardStats(),
17486
18027
  fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
18028
+ recordingAudio: createRecordingAudioRuntime(client),
17487
18029
  initialView: parsed.initialView
17488
18030
  });
17489
18031
  return 0;
@@ -17509,7 +18051,7 @@ async function runCli(deps = {}) {
17509
18051
  return 0;
17510
18052
  }
17511
18053
  if (parsed.kind === "auth-logout") {
17512
- const cleared = await clearAuthConfig(deps.homeDir ?? os3.homedir());
18054
+ const cleared = await clearAuthConfig(deps.homeDir ?? os4.homedir());
17513
18055
  renderSuccess("auth logout", { loggedIn: false, origin: auth.origin, cleared }, render2);
17514
18056
  return 0;
17515
18057
  }
@@ -17520,7 +18062,7 @@ async function runCli(deps = {}) {
17520
18062
  hint: keychain.hint ?? "Run recappi auth login instead."
17521
18063
  });
17522
18064
  }
17523
- await saveAuthConfig(deps.homeDir ?? os3.homedir(), {
18065
+ await saveAuthConfig(deps.homeDir ?? os4.homedir(), {
17524
18066
  origin: auth.origin,
17525
18067
  token: keychain.token
17526
18068
  });