recappi 0.1.1 → 0.1.2

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
@@ -20,7 +20,7 @@ function Header({ active }) {
20
20
  ] }),
21
21
  /* @__PURE__ */ jsx(Tab, { num: "1", label: "Overview", active: active === "overview", enabled: true }),
22
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: false })
23
+ /* @__PURE__ */ jsx(Tab, { num: "3", label: "Recordings", active: active === "recordings", enabled: true })
24
24
  ] });
25
25
  }
26
26
  function Tab({
@@ -151,6 +151,36 @@ function resolveJobLinks(item, origin) {
151
151
  }
152
152
  return {};
153
153
  }
154
+ function resolveRecordingLinks(recordingId, origin) {
155
+ if (!recordingId) return {};
156
+ return { webUrl: `${origin}/recordings/${recordingId}` };
157
+ }
158
+ function recordingStatusStyle(status) {
159
+ switch (status) {
160
+ case "ready":
161
+ return { label: "Ready", color: "green", glyph: "\u2713" };
162
+ case "uploading":
163
+ return { label: "Uploading", color: "cyan", glyph: "\u2191" };
164
+ case "failed":
165
+ return { label: "Failed", color: "red", glyph: "\u2717" };
166
+ case "aborted":
167
+ return { label: "Aborted", color: "gray", glyph: "\u2022" };
168
+ default:
169
+ return { label: status, color: "white", glyph: "\u2022" };
170
+ }
171
+ }
172
+ function formatBytes2(bytes) {
173
+ if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "";
174
+ const units = ["B", "KB", "MB", "GB", "TB"];
175
+ let value = bytes;
176
+ let unit = 0;
177
+ while (value >= 1024 && unit < units.length - 1) {
178
+ value /= 1024;
179
+ unit += 1;
180
+ }
181
+ const rounded = value < 10 && unit > 0 ? value.toFixed(1) : String(Math.round(value));
182
+ return `${rounded}${units[unit]}`;
183
+ }
154
184
  var SPINNER_FRAMES;
155
185
  var init_format = __esm({
156
186
  "src/tui/format.ts"() {
@@ -212,83 +242,149 @@ var init_JobsView = __esm({
212
242
  }
213
243
  });
214
244
 
215
- // src/tui/OverviewView.tsx
245
+ // src/tui/RecordingRow.tsx
216
246
  import { Box as Box4, Text as Text4 } from "ink";
217
247
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
218
- function overviewActiveItems(items) {
219
- return items.filter((item) => item.status === "running" || item.status === "queued");
248
+ function recordingTitle2(item) {
249
+ return item.title || item.summaryTitle || item.recordingId;
250
+ }
251
+ function RecordingRow({
252
+ item,
253
+ selected,
254
+ nowMs
255
+ }) {
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 ");
262
+ return /* @__PURE__ */ jsxs3(Box4, { children: [
263
+ /* @__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 })
267
+ ] });
268
+ }
269
+ var init_RecordingRow = __esm({
270
+ "src/tui/RecordingRow.tsx"() {
271
+ "use strict";
272
+ init_format();
273
+ }
274
+ });
275
+
276
+ // src/tui/OverviewView.tsx
277
+ import { Box as Box5, Text as Text5 } from "ink";
278
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
279
+ function overviewRecentRecordings(recordings) {
280
+ return recordings.slice(0, RECENT_LIMIT);
220
281
  }
221
282
  function OverviewView({
222
- items,
283
+ recordings,
284
+ jobs,
285
+ stats,
223
286
  selectedIndex,
224
287
  spinnerFrame,
225
288
  nowMs
226
289
  }) {
227
- const counts = countJobs(items);
228
- const active = overviewActiveItems(items);
229
- const recent = items.filter((item) => item.status === "succeeded" || item.status === "failed").slice(0, 5);
230
- return /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
231
- /* @__PURE__ */ jsxs3(Text4, { children: [
232
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Jobs " }),
233
- /* @__PURE__ */ jsxs3(Text4, { color: "cyan", children: [
234
- counts.running,
235
- " running"
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;
296
+ 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
236
303
  ] }),
237
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " \xB7 " }),
238
- /* @__PURE__ */ jsxs3(Text4, { color: "yellow", children: [
239
- counts.queued,
240
- " queued"
241
- ] }),
242
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " \xB7 " }),
243
- /* @__PURE__ */ jsxs3(Text4, { color: "green", children: [
244
- counts.succeeded,
245
- " done"
246
- ] }),
247
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " \xB7 " }),
248
- /* @__PURE__ */ jsxs3(Text4, { color: "red", children: [
249
- counts.failed,
250
- " failed"
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
+ ] })
251
320
  ] })
252
321
  ] }),
253
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
254
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Active" }),
255
- active.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Nothing transcribing right now." }) : active.map((item, index) => /* @__PURE__ */ jsx4(
256
- JobRow,
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,
257
326
  {
258
327
  item,
259
328
  selected: index === selectedIndex,
260
- spinnerFrame
329
+ nowMs
261
330
  },
262
- item.jobId
331
+ item.recordingId
263
332
  ))
264
333
  ] }),
265
- recent.length > 0 ? /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
266
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Recent" }),
267
- recent.map((item) => {
268
- const style = statusStyle(item.status);
269
- const title = item.recording?.title ?? item.recordingId;
270
- const age = formatAge(item.finishedAt ?? item.enqueuedAt, nowMs);
271
- return /* @__PURE__ */ jsxs3(Box4, { children: [
272
- /* @__PURE__ */ jsx4(Text4, { children: " " }),
273
- /* @__PURE__ */ jsx4(Text4, { color: style.color, children: `${statusGlyph(item.status, 0)} ` }),
274
- /* @__PURE__ */ jsx4(Text4, { children: title }),
275
- age ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` ${age}` }) : null
276
- ] }, item.jobId);
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);
277
343
  })
278
344
  ] }) : null
279
345
  ] });
280
346
  }
347
+ var RECENT_LIMIT;
281
348
  var init_OverviewView = __esm({
282
349
  "src/tui/OverviewView.tsx"() {
283
350
  "use strict";
284
- init_JobRow();
351
+ init_RecordingRow();
285
352
  init_format();
353
+ RECENT_LIMIT = 6;
354
+ }
355
+ });
356
+
357
+ // src/tui/RecordingsView.tsx
358
+ 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,
363
+ nowMs
364
+ }) {
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
+ )) });
377
+ }
378
+ var init_RecordingsView = __esm({
379
+ "src/tui/RecordingsView.tsx"() {
380
+ "use strict";
381
+ init_RecordingRow();
286
382
  }
287
383
  });
288
384
 
289
385
  // src/tui/JobDetailView.tsx
290
- import { Box as Box5, Text as Text5 } from "ink";
291
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
386
+ import { Box as Box7, Text as Text7 } from "ink";
387
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
292
388
  function JobDetailView({
293
389
  item,
294
390
  origin,
@@ -298,13 +394,13 @@ function JobDetailView({
298
394
  const style = statusStyle(item.status);
299
395
  const links = resolveJobLinks(item, origin);
300
396
  const title = item.recording?.title ?? item.recordingId;
301
- return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 1, children: [
302
- /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
397
+ return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", paddingX: 1, children: [
398
+ /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
303
399
  "\u2039 Jobs / ",
304
400
  title
305
401
  ] }),
306
- /* @__PURE__ */ jsxs4(
307
- Box5,
402
+ /* @__PURE__ */ jsxs5(
403
+ Box7,
308
404
  {
309
405
  marginTop: 1,
310
406
  borderStyle: "round",
@@ -312,17 +408,17 @@ function JobDetailView({
312
408
  paddingX: 1,
313
409
  flexDirection: "column",
314
410
  children: [
315
- /* @__PURE__ */ jsxs4(Text5, { color: style.color, bold: true, children: [
411
+ /* @__PURE__ */ jsxs5(Text7, { color: style.color, bold: true, children: [
316
412
  style.label,
317
- item.provider ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${item.provider}` }) : null
413
+ item.provider ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` ${item.provider}` }) : null
318
414
  ] }),
319
- /* @__PURE__ */ jsx5(StatusLine, { item, spinnerFrame, nowMs })
415
+ /* @__PURE__ */ jsx7(StatusLine, { item, spinnerFrame, nowMs })
320
416
  ]
321
417
  }
322
418
  ),
323
- /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
324
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Timeline" }),
325
- /* @__PURE__ */ jsx5(
419
+ /* @__PURE__ */ jsxs5(Box7, { marginTop: 1, flexDirection: "column", children: [
420
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Timeline" }),
421
+ /* @__PURE__ */ jsx7(
326
422
  TimelineRow,
327
423
  {
328
424
  label: "Enqueued",
@@ -331,7 +427,7 @@ function JobDetailView({
331
427
  nowMs
332
428
  }
333
429
  ),
334
- /* @__PURE__ */ jsx5(
430
+ /* @__PURE__ */ jsx7(
335
431
  TimelineRow,
336
432
  {
337
433
  label: "Started",
@@ -340,7 +436,7 @@ function JobDetailView({
340
436
  nowMs
341
437
  }
342
438
  ),
343
- /* @__PURE__ */ jsx5(
439
+ /* @__PURE__ */ jsx7(
344
440
  TimelineRow,
345
441
  {
346
442
  label: item.status === "failed" ? "Failed" : item.status === "running" ? "Transcribing" : "Finished",
@@ -352,21 +448,21 @@ function JobDetailView({
352
448
  }
353
449
  )
354
450
  ] }),
355
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
356
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Recording " }),
451
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
452
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Recording " }),
357
453
  title,
358
- item.recording?.durationMs ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
454
+ item.recording?.durationMs ? /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: ` \xB7 ${formatClockMs(item.recording.durationMs)}` }) : null
359
455
  ] }) }),
360
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
361
- /* @__PURE__ */ jsx5(Text5, { color: links.webUrl ? "cyan" : "gray", children: "o open" }),
362
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
363
- /* @__PURE__ */ jsx5(Text5, { color: links.webUrl ? "cyan" : "gray", children: "w web" }),
364
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
365
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "m mac app (soon)" }),
366
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \xB7 " }),
367
- /* @__PURE__ */ jsx5(Text5, { color: links.webUrl ? "cyan" : "gray", children: "c copy" })
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" })
368
464
  ] }) }),
369
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
465
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { dimColor: true, children: [
370
466
  "esc back \xB7 t transcript",
371
467
  item.transcriptId ? "" : " (when ready)",
372
468
  " \xB7 q quit"
@@ -383,24 +479,24 @@ function StatusLine({
383
479
  const elapsed = item.startedAt ? ` \xB7 ${formatClockMs(nowMs - item.startedAt)} elapsed` : "";
384
480
  if (fraction != null) {
385
481
  const pct = Math.round(fraction * 100);
386
- return /* @__PURE__ */ jsxs4(Text5, { children: [
482
+ return /* @__PURE__ */ jsxs5(Text7, { children: [
387
483
  `${progressBar(fraction)} ${pct}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
388
484
  item.recording?.durationMs
389
485
  )}`,
390
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: elapsed })
486
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
391
487
  ] });
392
488
  }
393
489
  const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"][spinnerFrame % 10];
394
- return /* @__PURE__ */ jsxs4(Text5, { children: [
490
+ return /* @__PURE__ */ jsxs5(Text7, { children: [
395
491
  `${spinner} transcribing\u2026`,
396
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: elapsed })
492
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: elapsed })
397
493
  ] });
398
494
  }
399
495
  if (item.status === "succeeded")
400
- return /* @__PURE__ */ jsx5(Text5, { children: item.transcriptId ? "transcript ready" : "done" });
401
- if (item.status === "queued") return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "waiting to start\u2026" });
402
- if (item.status === "failed") return /* @__PURE__ */ jsx5(Text5, { color: "red", children: "transcription failed" });
403
- return /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: item.status });
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 });
404
500
  }
405
501
  function TimelineRow({
406
502
  label,
@@ -413,10 +509,10 @@ function TimelineRow({
413
509
  const glyph = failed ? "\u2717" : done ? "\u2713" : running ? "\u280B" : "\u25CB";
414
510
  const color = failed ? "red" : done ? "green" : running ? "cyan" : "gray";
415
511
  const age = at ? formatAge(at, nowMs) : running ? "now" : "";
416
- return /* @__PURE__ */ jsxs4(Box5, { children: [
417
- /* @__PURE__ */ jsx5(Text5, { color, children: ` ${glyph} ` }),
418
- /* @__PURE__ */ jsx5(Text5, { dimColor: !done && !running, children: label }),
419
- age ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: ` ${age}` }) : null
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
420
516
  ] });
421
517
  }
422
518
  var init_JobDetailView = __esm({
@@ -426,46 +522,111 @@ var init_JobDetailView = __esm({
426
522
  }
427
523
  });
428
524
 
525
+ // 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";
528
+ function RecordingDetailView({
529
+ item,
530
+ nowMs
531
+ }) {
532
+ const style = recordingStatusStyle(item.status);
533
+ const links = resolveRecordingLinks(item.recordingId, item.origin);
534
+ const title = recordingTitle2(item);
535
+ const meta3 = [
536
+ item.durationMs ? formatClockMs(item.durationMs) : void 0,
537
+ formatBytes2(item.sizeBytes) || void 0,
538
+ item.contentType || void 0
539
+ ].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
544
+ ] }),
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"
579
+ ] }) })
580
+ ] });
581
+ }
582
+ var init_RecordingDetailView = __esm({
583
+ "src/tui/RecordingDetailView.tsx"() {
584
+ "use strict";
585
+ init_format();
586
+ init_RecordingRow();
587
+ }
588
+ });
589
+
429
590
  // src/tui/TranscriptView.tsx
430
- import { Box as Box6, Text as Text6 } from "ink";
431
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
591
+ import { Box as Box9, Text as Text9 } from "ink";
592
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
432
593
  function TranscriptView({ loading, data, error: error51 }) {
433
594
  if (loading) {
434
- return /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "Loading transcript\u2026" }) });
595
+ return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Loading transcript\u2026" }) });
435
596
  }
436
597
  if (error51) {
437
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: 1, children: [
438
- /* @__PURE__ */ jsxs5(Text6, { color: "red", children: [
598
+ return /* @__PURE__ */ jsxs7(Box9, { flexDirection: "column", paddingX: 1, children: [
599
+ /* @__PURE__ */ jsxs7(Text9, { color: "red", children: [
439
600
  "! ",
440
601
  error51
441
602
  ] }),
442
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "q / esc / \u2190 back" })
603
+ /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" })
443
604
  ] });
444
605
  }
445
606
  if (!data) {
446
- return /* @__PURE__ */ jsx6(Box6, { paddingX: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "No transcript." }) });
607
+ return /* @__PURE__ */ jsx9(Box9, { paddingX: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "No transcript." }) });
447
608
  }
448
609
  const summary = data.summary;
449
610
  const showSummary = summary?.status === "succeeded";
450
- return /* @__PURE__ */ jsxs5(Box6, { flexDirection: "column", paddingX: 1, children: [
451
- /* @__PURE__ */ jsx6(Text6, { bold: true, color: "magenta", children: summary?.title ?? "Transcript" }),
452
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, flexDirection: "column", children: data.segments.length === 0 ? /* @__PURE__ */ jsx6(Text6, { children: data.text }) : data.segments.slice(0, 200).map((segment, index) => /* @__PURE__ */ jsxs5(Text6, { children: [
453
- /* @__PURE__ */ jsxs5(Text6, { dimColor: true, children: [
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: [
454
615
  "[",
455
616
  formatClockMs(segment.startMs),
456
617
  "] "
457
618
  ] }),
458
- segment.speaker ? /* @__PURE__ */ jsxs5(Text6, { color: "cyan", children: [
619
+ segment.speaker ? /* @__PURE__ */ jsxs7(Text9, { color: "cyan", children: [
459
620
  segment.speaker,
460
621
  ": "
461
622
  ] }) : null,
462
623
  segment.text
463
624
  ] }, index)) }),
464
- showSummary && summary?.tldr ? /* @__PURE__ */ jsxs5(Box6, { marginTop: 1, flexDirection: "column", children: [
465
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Summary" }),
466
- /* @__PURE__ */ jsx6(Text6, { children: summary.tldr })
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 })
467
628
  ] }) : null,
468
- /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "q / esc / \u2190 back" }) })
629
+ /* @__PURE__ */ jsx9(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "q / esc / \u2190 back" }) })
469
630
  ] });
470
631
  }
471
632
  var init_TranscriptView = __esm({
@@ -477,11 +638,13 @@ var init_TranscriptView = __esm({
477
638
 
478
639
  // src/tui/AppShell.tsx
479
640
  import { useCallback, useEffect, useState } from "react";
480
- import { Box as Box7, Text as Text7, useApp, useInput } from "ink";
481
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
641
+ import { Box as Box10, Text as Text10, useApp, useInput } from "ink";
642
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
482
643
  function AppShell({
483
644
  fetchJobs,
484
645
  fetchTranscript,
646
+ fetchRecordings,
647
+ fetchDashboardStats,
485
648
  initialView = "overview",
486
649
  openUrl: openUrl2,
487
650
  copyText: copyText2,
@@ -490,41 +653,48 @@ function AppShell({
490
653
  spinnerMs = 80
491
654
  }) {
492
655
  const { exit } = useApp();
493
- const [items, setItems] = useState([]);
656
+ const [jobs, setJobs] = useState([]);
657
+ const [recordings, setRecordings] = useState([]);
658
+ const [stats, setStats] = useState(void 0);
494
659
  const [origin, setOrigin] = useState("");
495
- const [stack, setStack] = useState([
496
- { kind: initialView === "jobs" ? "jobs" : "overview" }
497
- ]);
660
+ const [stack, setStack] = useState([{ kind: initialView }]);
498
661
  const [selected, setSelected] = useState(0);
499
662
  const [spinnerFrame, setSpinnerFrame] = useState(0);
500
663
  const [loadError, setLoadError] = useState(void 0);
501
664
  const [notice, setNotice] = useState(void 0);
502
665
  const screen = stack[stack.length - 1];
503
666
  const refresh = useCallback(async () => {
504
- try {
505
- const data = await fetchJobs();
506
- setItems(data.items);
507
- setOrigin(data.origin);
667
+ const [jobsR, recR, statsR] = await Promise.allSettled([
668
+ fetchJobs(),
669
+ fetchRecordings ? fetchRecordings() : Promise.resolve(void 0),
670
+ fetchDashboardStats ? fetchDashboardStats() : Promise.resolve(void 0)
671
+ ]);
672
+ if (jobsR.status === "fulfilled") {
673
+ setJobs(jobsR.value.items);
674
+ setOrigin(jobsR.value.origin);
508
675
  setLoadError(void 0);
509
- } catch (error51) {
510
- setLoadError(error51 instanceof Error ? error51.message : String(error51));
676
+ } else {
677
+ setLoadError(jobsR.reason instanceof Error ? jobsR.reason.message : String(jobsR.reason));
511
678
  }
512
- }, [fetchJobs]);
679
+ if (recR.status === "fulfilled" && recR.value) setRecordings(recR.value.items);
680
+ if (statsR.status === "fulfilled" && statsR.value) setStats(statsR.value);
681
+ }, [fetchJobs, fetchRecordings, fetchDashboardStats]);
513
682
  useEffect(() => {
514
683
  void refresh();
515
684
  const id = setInterval(() => void refresh(), pollMs);
516
685
  return () => clearInterval(id);
517
686
  }, [refresh, pollMs]);
518
- const hasRunning = items.some((item) => item.status === "running");
687
+ const hasRunning = jobs.some((item) => item.status === "running");
519
688
  useEffect(() => {
520
689
  if (!hasRunning) return;
521
690
  const id = setInterval(() => setSpinnerFrame((f) => f + 1), spinnerMs);
522
691
  return () => clearInterval(id);
523
692
  }, [hasRunning, spinnerMs]);
524
- const currentList = screen.kind === "overview" ? overviewActiveItems(items) : items;
693
+ const recordingList = screen.kind === "overview" ? overviewRecentRecordings(recordings) : recordings;
694
+ const listLength = screen.kind === "jobs" ? jobs.length : recordingList.length;
525
695
  useEffect(() => {
526
- setSelected((i) => Math.max(0, Math.min(i, Math.max(0, currentList.length - 1))));
527
- }, [currentList.length]);
696
+ setSelected((i) => Math.max(0, Math.min(i, Math.max(0, listLength - 1))));
697
+ }, [listLength]);
528
698
  const openTranscript = useCallback(
529
699
  async (transcriptId) => {
530
700
  setStack((st) => [...st, { kind: "transcript", loading: true }]);
@@ -552,33 +722,46 @@ function AppShell({
552
722
  const back = () => setStack((st) => st.length > 1 ? st.slice(0, -1) : st);
553
723
  useInput((input, key) => {
554
724
  setNotice(void 0);
555
- if (input === "q") {
556
- exit();
557
- return;
558
- }
559
- if (key.escape || key.leftArrow) {
560
- back();
561
- return;
562
- }
725
+ if (input === "q") return exit();
726
+ if (key.escape || key.leftArrow) return back();
563
727
  if (input === "1") return goTab("overview");
564
728
  if (input === "2") return goTab("jobs");
565
- if (input === "r") {
566
- void refresh();
729
+ if (input === "3") return goTab("recordings");
730
+ if (input === "r") return void refresh();
731
+ if (screen.kind === "overview" || screen.kind === "recordings") {
732
+ 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];
736
+ if (key.return && rec)
737
+ setStack((st) => [...st, { kind: "recordingDetail", recordingId: rec.recordingId }]);
738
+ if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
567
739
  return;
568
740
  }
569
- if (screen.kind === "overview" || screen.kind === "jobs") {
741
+ if (screen.kind === "jobs") {
570
742
  if (key.upArrow || input === "k") setSelected((i) => Math.max(0, i - 1));
571
- if (key.downArrow || input === "j")
572
- setSelected((i) => Math.min(currentList.length - 1, i + 1));
573
- const item = currentList[selected];
574
- if (key.return && item) setStack((st) => [...st, { kind: "jobDetail", jobId: item.jobId }]);
575
- if (input === "t" && item?.transcriptId) void openTranscript(item.transcriptId);
743
+ if (key.downArrow || input === "j") setSelected((i) => Math.min(jobs.length - 1, i + 1));
744
+ const job = jobs[selected];
745
+ if (key.return && job) setStack((st) => [...st, { kind: "jobDetail", jobId: job.jobId }]);
746
+ if (input === "t" && job?.transcriptId) void openTranscript(job.transcriptId);
576
747
  return;
577
748
  }
578
749
  if (screen.kind === "jobDetail") {
579
- const item = items.find((i) => i.jobId === screen.jobId);
580
- const links = item ? resolveJobLinks(item, origin) : {};
581
- if (input === "t" && item?.transcriptId) void openTranscript(item.transcriptId);
750
+ const job = jobs.find((j) => j.jobId === screen.jobId);
751
+ const links = job ? resolveJobLinks(job, origin) : {};
752
+ if (input === "t" && job?.transcriptId) void openTranscript(job.transcriptId);
753
+ else if ((input === "o" || input === "w") && links.webUrl) openUrl2?.(links.webUrl);
754
+ else if (input === "m") setNotice("Mac app deeplink not available yet");
755
+ else if (input === "c" && links.webUrl) {
756
+ copyText2?.(links.webUrl);
757
+ setNotice("Link copied");
758
+ }
759
+ return;
760
+ }
761
+ if (screen.kind === "recordingDetail") {
762
+ const rec = recordings.find((r) => r.recordingId === screen.recordingId);
763
+ const links = rec ? resolveRecordingLinks(rec.recordingId, rec.origin) : {};
764
+ if (input === "t" && rec?.activeTranscriptId) void openTranscript(rec.activeTranscriptId);
582
765
  else if ((input === "o" || input === "w") && links.webUrl) openUrl2?.(links.webUrl);
583
766
  else if (input === "m") setNotice("Mac app deeplink not available yet");
584
767
  else if (input === "c" && links.webUrl) {
@@ -589,43 +772,56 @@ function AppShell({
589
772
  }
590
773
  });
591
774
  if (screen.kind === "transcript") {
592
- return /* @__PURE__ */ jsx7(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
775
+ return /* @__PURE__ */ jsx10(TranscriptView, { loading: screen.loading, data: screen.data, error: screen.error });
593
776
  }
594
777
  if (screen.kind === "jobDetail") {
595
- const item = items.find((i) => i.jobId === screen.jobId);
596
- if (!item) {
597
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 1, children: [
598
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Job no longer in the list." }),
599
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "esc back \xB7 q quit" })
600
- ] });
601
- }
602
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", children: [
603
- /* @__PURE__ */ jsx7(JobDetailView, { item, origin, spinnerFrame, nowMs: now() }),
604
- notice ? /* @__PURE__ */ jsx7(Box7, { paddingX: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "green", children: notice }) }) : null
605
- ] });
606
- }
607
- const tab = screen.kind === "jobs" ? "jobs" : "overview";
608
- return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", paddingX: 1, children: [
609
- /* @__PURE__ */ jsx7(Header, { active: tab }),
610
- screen.kind === "overview" ? /* @__PURE__ */ jsx7(
778
+ 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() }) });
781
+ }
782
+ if (screen.kind === "recordingDetail") {
783
+ 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(
611
792
  OverviewView,
612
793
  {
613
- items,
794
+ recordings,
795
+ jobs,
796
+ stats,
614
797
  selectedIndex: selected,
615
798
  spinnerFrame,
616
799
  nowMs: now()
617
800
  }
618
- ) : /* @__PURE__ */ jsx7(JobsView, { items, selectedIndex: selected, spinnerFrame }),
619
- loadError && items.length === 0 ? /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs6(Text7, { color: "red", children: [
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: [
620
803
  "! ",
621
804
  loadError
622
805
  ] }) }) : null,
623
- /* @__PURE__ */ jsx7(
624
- Footer,
625
- {
626
- keys: screen.kind === "jobs" ? "1/2 tabs \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 r refresh \xB7 q quit" : "1/2 tabs \xB7 \u2191\u2193 select \xB7 \u23CE job \xB7 t transcript \xB7 q quit"
627
- }
628
- )
806
+ /* @__PURE__ */ jsx10(Footer, { keys: footerKeys })
807
+ ] });
808
+ }
809
+ function Detail({
810
+ notice,
811
+ children
812
+ }) {
813
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", children: [
814
+ children,
815
+ notice ? /* @__PURE__ */ jsx10(Box10, { paddingX: 1, children: /* @__PURE__ */ jsx10(Text10, { color: "green", children: notice }) }) : null
816
+ ] });
817
+ }
818
+ function Missing({ label }) {
819
+ return /* @__PURE__ */ jsxs8(Box10, { flexDirection: "column", paddingX: 1, children: [
820
+ /* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
821
+ label,
822
+ " no longer in the list."
823
+ ] }),
824
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "esc back \xB7 q quit" })
629
825
  ] });
630
826
  }
631
827
  var init_AppShell = __esm({
@@ -634,7 +830,9 @@ var init_AppShell = __esm({
634
830
  init_chrome();
635
831
  init_JobsView();
636
832
  init_OverviewView();
833
+ init_RecordingsView();
637
834
  init_JobDetailView();
835
+ init_RecordingDetailView();
638
836
  init_TranscriptView();
639
837
  init_format();
640
838
  }
@@ -672,6 +870,8 @@ async function runDashboard(deps) {
672
870
  React2.createElement(AppShell, {
673
871
  fetchJobs: deps.fetchJobs,
674
872
  fetchTranscript: deps.fetchTranscript,
873
+ fetchRecordings: deps.fetchRecordings,
874
+ fetchDashboardStats: deps.fetchDashboardStats,
675
875
  initialView: deps.initialView ?? "overview",
676
876
  openUrl,
677
877
  copyText
@@ -15340,6 +15540,45 @@ var jobListDataSchema = external_exports.object({
15340
15540
  limit: external_exports.number().int().positive(),
15341
15541
  origin: external_exports.string()
15342
15542
  });
15543
+ var recordingDataSchema = external_exports.object({
15544
+ recordingId: external_exports.string(),
15545
+ title: external_exports.string().nullable().optional(),
15546
+ summaryTitle: external_exports.string().nullable().optional(),
15547
+ status: recordingStatusSchema,
15548
+ durationMs: external_exports.number().int().nonnegative().nullable().optional(),
15549
+ sizeBytes: external_exports.number().int().nonnegative().nullable().optional(),
15550
+ contentType: external_exports.string().optional(),
15551
+ activeTranscriptId: external_exports.string().nullable().optional(),
15552
+ createdAt: external_exports.number().int(),
15553
+ updatedAt: external_exports.number().int(),
15554
+ origin: external_exports.string()
15555
+ });
15556
+ var recordingListDataSchema = external_exports.object({
15557
+ items: external_exports.array(recordingDataSchema),
15558
+ limit: external_exports.number().int().positive(),
15559
+ nextCursor: external_exports.string().nullable().optional(),
15560
+ totalCount: external_exports.number().int().nonnegative().optional(),
15561
+ origin: external_exports.string()
15562
+ });
15563
+ var dashboardStatsDataSchema = external_exports.object({
15564
+ origin: external_exports.string(),
15565
+ recordings: external_exports.object({
15566
+ total: external_exports.number().int().nonnegative(),
15567
+ ready: external_exports.number().int().nonnegative(),
15568
+ uploading: external_exports.number().int().nonnegative(),
15569
+ failed: external_exports.number().int().nonnegative(),
15570
+ aborted: external_exports.number().int().nonnegative(),
15571
+ totalDurationMs: external_exports.number().int().nonnegative(),
15572
+ totalSizeBytes: external_exports.number().int().nonnegative()
15573
+ }),
15574
+ jobs: external_exports.object({
15575
+ active: external_exports.number().int().nonnegative(),
15576
+ queued: external_exports.number().int().nonnegative(),
15577
+ running: external_exports.number().int().nonnegative(),
15578
+ succeeded: external_exports.number().int().nonnegative(),
15579
+ failed: external_exports.number().int().nonnegative()
15580
+ })
15581
+ });
15343
15582
  var transcriptSegmentSchema = external_exports.object({
15344
15583
  startMs: external_exports.number().nonnegative(),
15345
15584
  endMs: external_exports.number().nonnegative(),
@@ -16130,6 +16369,30 @@ var RecappiApiClient = class {
16130
16369
  origin: this.auth.origin
16131
16370
  });
16132
16371
  }
16372
+ async listRecordings(opts) {
16373
+ const params = new URLSearchParams({ limit: String(opts.limit) });
16374
+ if (opts.cursor) params.set("cursor", opts.cursor);
16375
+ if (opts.search) params.set("search", opts.search);
16376
+ const parsed = await this.getJson(`/api/recordings?${params}`);
16377
+ const items = Array.isArray(parsed.items) ? parsed.items.filter(isRecord2).map((row) => mapRecording(row, this.auth.origin)) : [];
16378
+ return recordingListDataSchema.parse({
16379
+ items,
16380
+ limit: opts.limit,
16381
+ ...typeof parsed.nextCursor === "string" || parsed.nextCursor === null ? { nextCursor: parsed.nextCursor } : {},
16382
+ ...typeof parsed.totalCount === "number" ? { totalCount: parsed.totalCount } : {},
16383
+ origin: this.auth.origin
16384
+ });
16385
+ }
16386
+ async getRecording(recordingId) {
16387
+ const parsed = await this.getJson(
16388
+ `/api/recordings/${encodeURIComponent(recordingId)}`
16389
+ );
16390
+ return mapRecording(parsed, this.auth.origin);
16391
+ }
16392
+ async dashboardStats() {
16393
+ const parsed = await this.getJson("/api/dashboard/stats");
16394
+ return mapDashboardStats(parsed, this.auth.origin);
16395
+ }
16133
16396
  async uploadPathBatch(opts) {
16134
16397
  const files = await collectAudioFiles(opts.inputPath);
16135
16398
  if (files.length === 0) {
@@ -16510,9 +16773,59 @@ function mapJobListItem(row) {
16510
16773
  }
16511
16774
  };
16512
16775
  }
16776
+ function mapRecording(row, origin) {
16777
+ const recordingId = stringValue(row.id) ?? stringValue(row.recordingId);
16778
+ const status = stringValue(row.status);
16779
+ const createdAt = numberValue(row.createdAt);
16780
+ const updatedAt = numberValue(row.updatedAt);
16781
+ if (!recordingId) {
16782
+ throw cliError("cloud.invalid_response", "Recording response was missing id.");
16783
+ }
16784
+ if (!status) {
16785
+ throw cliError("cloud.invalid_response", "Recording response was missing status.");
16786
+ }
16787
+ if (createdAt === void 0 || updatedAt === void 0) {
16788
+ throw cliError("cloud.invalid_response", "Recording response was missing timestamps.");
16789
+ }
16790
+ return recordingDataSchema.parse({
16791
+ recordingId,
16792
+ ...typeof row.title === "string" || row.title === null ? { title: row.title } : {},
16793
+ ...typeof row.summaryTitle === "string" || row.summaryTitle === null ? { summaryTitle: row.summaryTitle } : {},
16794
+ status,
16795
+ ...typeof row.durationMs === "number" || row.durationMs === null ? { durationMs: row.durationMs } : {},
16796
+ ...typeof row.sizeBytes === "number" || row.sizeBytes === null ? { sizeBytes: row.sizeBytes } : {},
16797
+ ...typeof row.contentType === "string" ? { contentType: row.contentType } : {},
16798
+ ...typeof row.activeTranscriptId === "string" || row.activeTranscriptId === null ? { activeTranscriptId: row.activeTranscriptId } : {},
16799
+ createdAt,
16800
+ updatedAt,
16801
+ origin
16802
+ });
16803
+ }
16804
+ function mapDashboardStats(row, origin) {
16805
+ return dashboardStatsDataSchema.parse({
16806
+ origin,
16807
+ recordings: mapCountObject(row.recordings, [
16808
+ "total",
16809
+ "ready",
16810
+ "uploading",
16811
+ "failed",
16812
+ "aborted",
16813
+ "totalDurationMs",
16814
+ "totalSizeBytes"
16815
+ ]),
16816
+ jobs: mapCountObject(row.jobs, ["active", "queued", "running", "succeeded", "failed"])
16817
+ });
16818
+ }
16819
+ function mapCountObject(value, keys) {
16820
+ const source = isRecord2(value) ? value : {};
16821
+ return Object.fromEntries(keys.map((key) => [key, numberValue(source[key]) ?? 0]));
16822
+ }
16513
16823
  function stringValue(value) {
16514
16824
  return typeof value === "string" ? value : void 0;
16515
16825
  }
16826
+ function numberValue(value) {
16827
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
16828
+ }
16516
16829
  function parseSummaryStatus(value) {
16517
16830
  const allowed = /* @__PURE__ */ new Set([
16518
16831
  "pending",
@@ -16672,6 +16985,56 @@ function renderHumanSuccess(command, data, opts) {
16672
16985
  renderTranscriptHuman(data, opts);
16673
16986
  return;
16674
16987
  }
16988
+ if (command === "recordings list" && isRecord3(data) && Array.isArray(data.items)) {
16989
+ opts.stdout("Recordings:\n");
16990
+ for (const item of data.items) {
16991
+ if (!isRecord3(item)) continue;
16992
+ opts.stdout(` ${recordingLabel(item)}
16993
+ `);
16994
+ }
16995
+ if (data.items.length === 0) opts.stdout(" No recordings found.\n");
16996
+ if (typeof data.nextCursor === "string") {
16997
+ opts.stdout(`
16998
+ Next cursor: ${data.nextCursor}
16999
+ `);
17000
+ }
17001
+ return;
17002
+ }
17003
+ if (command === "recordings get" && isRecord3(data)) {
17004
+ opts.stdout(`${recordingTitle(data)}
17005
+ `);
17006
+ opts.stdout(` recordingId: ${String(data.recordingId)}
17007
+ `);
17008
+ if (typeof data.status === "string") opts.stdout(` status: ${data.status}
17009
+ `);
17010
+ if (typeof data.durationMs === "number")
17011
+ opts.stdout(` duration: ${formatDurationMs(data.durationMs)}
17012
+ `);
17013
+ if (typeof data.sizeBytes === "number") opts.stdout(` size: ${formatBytes(data.sizeBytes)}
17014
+ `);
17015
+ if (typeof data.activeTranscriptId === "string") {
17016
+ opts.stdout(` activeTranscriptId: ${data.activeTranscriptId}
17017
+ `);
17018
+ opts.stdout(`
17019
+ Next:
17020
+ recappi transcript get ${data.activeTranscriptId}
17021
+ `);
17022
+ }
17023
+ return;
17024
+ }
17025
+ if (command === "dashboard stats" && isRecord3(data)) {
17026
+ const recordings = isRecord3(data.recordings) ? data.recordings : {};
17027
+ const jobs = isRecord3(data.jobs) ? data.jobs : {};
17028
+ opts.stdout(
17029
+ `Recordings: ${numberText(recordings.total)} total, ${numberText(recordings.ready)} ready
17030
+ `
17031
+ );
17032
+ opts.stdout(
17033
+ `Jobs: ${numberText(jobs.active)} active (${numberText(jobs.queued)} queued, ${numberText(jobs.running)} running)
17034
+ `
17035
+ );
17036
+ return;
17037
+ }
16675
17038
  if (command === "upload" && isUploadBatch(data)) {
16676
17039
  if (data.successes.length > 0) {
16677
17040
  opts.stdout(data.successes.length === 1 ? "Upload complete\n" : "Uploads complete\n");
@@ -16852,6 +17215,37 @@ Summary:
16852
17215
  }
16853
17216
  }
16854
17217
  }
17218
+ function recordingLabel(item) {
17219
+ const id = typeof item.recordingId === "string" ? item.recordingId : "unknown";
17220
+ const status = typeof item.status === "string" ? item.status : "unknown";
17221
+ const duration3 = typeof item.durationMs === "number" ? ` \xB7 ${formatDurationMs(item.durationMs)}` : "";
17222
+ return `${recordingTitle(item)} (${status}, ${id}${duration3})`;
17223
+ }
17224
+ function recordingTitle(item) {
17225
+ for (const key of ["title", "summaryTitle"]) {
17226
+ const value = item[key];
17227
+ if (typeof value === "string" && value.trim()) return value.trim();
17228
+ }
17229
+ return "Untitled recording";
17230
+ }
17231
+ function numberText(value) {
17232
+ return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "0";
17233
+ }
17234
+ function formatDurationMs(ms) {
17235
+ return formatClock(ms / 1e3);
17236
+ }
17237
+ function formatBytes(bytes) {
17238
+ if (bytes < 1024) return `${bytes} B`;
17239
+ const units = ["KB", "MB", "GB", "TB"];
17240
+ let value = bytes / 1024;
17241
+ let unit = units[0];
17242
+ for (const next of units.slice(1)) {
17243
+ if (value < 1024) break;
17244
+ value /= 1024;
17245
+ unit = next;
17246
+ }
17247
+ return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${unit}`;
17248
+ }
16855
17249
  function formatClock(seconds) {
16856
17250
  const total = Math.max(0, Math.floor(seconds));
16857
17251
  const hours = Math.floor(total / 3600);
@@ -16942,7 +17336,10 @@ var COMMAND_DATA_SCHEMAS = {
16942
17336
  "auth import-macos": authImportDataSchema,
16943
17337
  "auth status": authStatusDataSchema,
16944
17338
  doctor: doctorDataSchema,
17339
+ "dashboard stats": dashboardStatsDataSchema,
16945
17340
  upload: uploadBatchDataSchema,
17341
+ "recordings get": recordingDataSchema,
17342
+ "recordings list": recordingListDataSchema,
16946
17343
  "jobs list": jobListDataSchema,
16947
17344
  "jobs wait": jobDataSchema,
16948
17345
  "transcript get": transcriptDataSchema
@@ -17084,6 +17481,8 @@ async function runCli(deps = {}) {
17084
17481
  const runDashboard2 = deps.runDashboard ?? (await Promise.resolve().then(() => (init_tui(), tui_exports))).runDashboard;
17085
17482
  await runDashboard2({
17086
17483
  fetchJobs: () => client.listJobs({ status: "active", limit: 20 }),
17484
+ fetchRecordings: () => client.listRecordings({ limit: 20 }),
17485
+ fetchDashboardStats: () => client.dashboardStats(),
17087
17486
  fetchTranscript: (transcriptId) => client.getTranscript(transcriptId),
17088
17487
  initialView: parsed.initialView
17089
17488
  });
@@ -17181,6 +17580,25 @@ async function runCli(deps = {}) {
17181
17580
  renderSuccess("jobs list", data, render2);
17182
17581
  return 0;
17183
17582
  }
17583
+ if (parsed.kind === "recordings-list") {
17584
+ const data = await client.listRecordings({
17585
+ limit: parsed.limit,
17586
+ cursor: parsed.cursor,
17587
+ search: parsed.search
17588
+ });
17589
+ renderSuccess("recordings list", data, render2);
17590
+ return 0;
17591
+ }
17592
+ if (parsed.kind === "recordings-get") {
17593
+ const data = await client.getRecording(parsed.recordingId);
17594
+ renderSuccess("recordings get", data, render2);
17595
+ return 0;
17596
+ }
17597
+ if (parsed.kind === "dashboard-stats") {
17598
+ const data = await client.dashboardStats();
17599
+ renderSuccess("dashboard stats", data, render2);
17600
+ return 0;
17601
+ }
17184
17602
  if (parsed.kind === "transcript-get") {
17185
17603
  const data = await client.getTranscript(parsed.transcriptId);
17186
17604
  renderSuccess("transcript get", data, render2);
@@ -17402,6 +17820,44 @@ Agent mode:
17402
17820
  document: buildSchemaDocument(program)
17403
17821
  });
17404
17822
  });
17823
+ const dashboard = program.command("dashboard").description("Dashboard data commands");
17824
+ addCommonOptions(dashboard);
17825
+ const dashboardStats = dashboard.command("stats").description("Fetch dashboard counters");
17826
+ addCommonOptions(dashboardStats);
17827
+ dashboardStats.action((_options, command) => {
17828
+ onSelect({
17829
+ kind: "dashboard-stats",
17830
+ options: collectGlobalOptions(command),
17831
+ commandName: "dashboard stats"
17832
+ });
17833
+ });
17834
+ const recordings = program.command("recordings").description("Recording commands");
17835
+ addCommonOptions(recordings);
17836
+ const recordingsList = recordings.command("list").description("List recent recordings").option("--limit <n>", "number of recordings to show", parseLimitOption("--limit", 1, 100), 20).option("--cursor <cursor>", "pagination cursor", parseStringOption("--cursor")).option("--search <query>", "search recordings and transcripts", parseStringOption("--search"));
17837
+ addCommonOptions(recordingsList);
17838
+ recordingsList.action((_options, command) => {
17839
+ const opts = command.opts();
17840
+ onSelect({
17841
+ kind: "recordings-list",
17842
+ options: collectGlobalOptions(command),
17843
+ commandName: "recordings list",
17844
+ limit: opts.limit ?? 20,
17845
+ ...typeof opts.cursor === "string" ? { cursor: opts.cursor } : {},
17846
+ ...typeof opts.search === "string" ? { search: opts.search } : {}
17847
+ });
17848
+ });
17849
+ const recordingsGet = recordings.command("get <recordingId>").description("Fetch a recording by recording id");
17850
+ addCommonOptions(recordingsGet);
17851
+ recordingsGet.action(
17852
+ (recordingId, _options, command) => {
17853
+ onSelect({
17854
+ kind: "recordings-get",
17855
+ options: collectGlobalOptions(command),
17856
+ commandName: "recordings get",
17857
+ recordingId
17858
+ });
17859
+ }
17860
+ );
17405
17861
  const transcript = program.command("transcript").description("Transcript commands");
17406
17862
  addCommonOptions(transcript);
17407
17863
  const transcriptGet = transcript.command("get <transcriptId>").description("Fetch a transcript by transcript id");
@@ -17516,7 +17972,9 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
17516
17972
  "--provider",
17517
17973
  "--prompt",
17518
17974
  "--status",
17519
- "--limit"
17975
+ "--limit",
17976
+ "--cursor",
17977
+ "--search"
17520
17978
  ]);
17521
17979
  function hasCommandToken(argv) {
17522
17980
  return commandTokens(argv).length > 0;