recappi 0.1.63 → 0.1.65

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
@@ -139,7 +139,7 @@ function formatClockMs(ms) {
139
139
  const ss = String(secs).padStart(2, "0");
140
140
  return hours > 0 ? `${hours}:${mm}:${ss}` : `${mm}:${ss}`;
141
141
  }
142
- function progressBar(fraction, width = 10) {
142
+ function progressBar2(fraction, width = 10) {
143
143
  const clamped = Math.max(0, Math.min(1, fraction));
144
144
  const filled = Math.round(clamped * width);
145
145
  return `[${"\u2588".repeat(filled)}${"\u2591".repeat(width - filled)}]`;
@@ -200,7 +200,7 @@ function jobDetail(item) {
200
200
  const fraction = transcribeFraction(item);
201
201
  if (fraction != null) {
202
202
  const pct2 = Math.round(fraction * 100);
203
- return `${progressBar(fraction)} ${String(pct2).padStart(3)}% ${formatClockMs(
203
+ return `${progressBar2(fraction)} ${String(pct2).padStart(3)}% ${formatClockMs(
204
204
  item.processedDurationMs
205
205
  )} / ${formatClockMs(item.recording?.durationMs)}`;
206
206
  }
@@ -335,7 +335,7 @@ function groupedListWindow(buckets, selected, budget) {
335
335
  }
336
336
  return listWindow(selected, total, 1);
337
337
  }
338
- function formatBytes2(bytes) {
338
+ function formatBytes3(bytes) {
339
339
  if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return "";
340
340
  const units = ["B", "KB", "MB", "GB", "TB"];
341
341
  let value = bytes;
@@ -647,16 +647,16 @@ function Usage({
647
647
  ] }),
648
648
  /* @__PURE__ */ jsxs3(Text3, { children: [
649
649
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Minutes " }),
650
- minutesCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : "cyan", children: `${progressBar(minutesUsed / Math.max(1, minutesCap), 12)} ` }) : null,
650
+ minutesCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : "cyan", children: `${progressBar2(minutesUsed / Math.max(1, minutesCap), 12)} ` }) : null,
651
651
  /* @__PURE__ */ jsx5(Text3, { color: billing.isOverMinutes ? "red" : void 0, children: `${Math.round(minutesUsed)}` }),
652
652
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${minutesCap != null ? Math.round(minutesCap) : "\u221E"} min` }),
653
653
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` (batch ${Math.round(billing.batchMinutesUsed)} \xB7 live ${Math.round(billing.realtimeMinutesUsed)})` })
654
654
  ] }),
655
655
  /* @__PURE__ */ jsxs3(Text3, { children: [
656
656
  /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: "Storage " }),
657
- storageCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : "cyan", children: `${progressBar(billing.storageBytes / Math.max(1, storageCap), 12)} ` }) : null,
658
- /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : void 0, children: formatBytes2(billing.storageBytes) }),
659
- /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${storageCap != null ? formatBytes2(storageCap) : "\u221E"}` })
657
+ storageCap != null ? /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : "cyan", children: `${progressBar2(billing.storageBytes / Math.max(1, storageCap), 12)} ` }) : null,
658
+ /* @__PURE__ */ jsx5(Text3, { color: billing.isOverStorage ? "red" : void 0, children: formatBytes3(billing.storageBytes) }),
659
+ /* @__PURE__ */ jsx5(Text3, { dimColor: true, children: ` / ${storageCap != null ? formatBytes3(storageCap) : "\u221E"}` })
660
660
  ] }),
661
661
  billing.isOverMinutes || billing.isOverStorage ? /* @__PURE__ */ jsxs3(Text3, { color: "red", children: [
662
662
  billing.isOverMinutes ? "Over minutes limit. " : "",
@@ -907,7 +907,7 @@ function PeekBody({
907
907
  const style = recordingStatusStyle(item.status);
908
908
  const meta3 = [
909
909
  item.durationMs ? formatClockMs(item.durationMs) : null,
910
- formatBytes2(item.sizeBytes) || null,
910
+ formatBytes3(item.sizeBytes) || null,
911
911
  formatAge(item.createdAt, nowMs)
912
912
  ].filter(Boolean).join(" \xB7 ");
913
913
  return /* @__PURE__ */ jsxs8(Fragment3, { children: [
@@ -1108,7 +1108,7 @@ function StatusLine({
1108
1108
  if (fraction != null) {
1109
1109
  const pct2 = Math.round(fraction * 100);
1110
1110
  return /* @__PURE__ */ jsxs10(Text11, { children: [
1111
- `${progressBar(fraction)} ${pct2}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
1111
+ `${progressBar2(fraction)} ${pct2}% ${formatClockMs(item.processedDurationMs)} / ${formatClockMs(
1112
1112
  item.recording?.durationMs
1113
1113
  )}`,
1114
1114
  /* @__PURE__ */ jsx13(Text11, { dimColor: true, children: elapsed })
@@ -1169,7 +1169,7 @@ function RecordingDetailView({
1169
1169
  const title = recordingTitle2(item);
1170
1170
  const meta3 = [
1171
1171
  item.durationMs ? formatClockMs(item.durationMs) : void 0,
1172
- formatBytes2(item.sizeBytes) || void 0,
1172
+ formatBytes3(item.sizeBytes) || void 0,
1173
1173
  item.contentType || void 0
1174
1174
  ].filter(Boolean).join(" \xB7 ");
1175
1175
  const ready = typeof transcript === "object";
@@ -1729,7 +1729,7 @@ function RecordFrame({
1729
1729
  ...captions.lines.filter((l) => l.translation).map((l) => trimLead2(l.translation)),
1730
1730
  ...captions.translationPartial ? [trimLead2(captions.translationPartial)] : []
1731
1731
  ] : [];
1732
- const status = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
1732
+ const status = telemetry.sizeBytes ? formatBytes3(telemetry.sizeBytes) : "";
1733
1733
  const sourceLine = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, status || null].filter(Boolean).join(" \xB7 ");
1734
1734
  return /* @__PURE__ */ jsxs15(Box16, { flexDirection: "column", paddingX: 1, height: size.rows, children: [
1735
1735
  /* @__PURE__ */ jsxs15(Box16, { justifyContent: "space-between", children: [
@@ -19296,6 +19296,13 @@ var RecappiApiClient = class {
19296
19296
  for (const filePath of files) {
19297
19297
  attemptedCount += 1;
19298
19298
  try {
19299
+ opts.onEvent?.({
19300
+ type: "progress",
19301
+ command: "upload",
19302
+ filePath,
19303
+ status: "checking_audio",
19304
+ message: `Checking ${filePath}`
19305
+ });
19299
19306
  const plan = await planAudioFile(filePath, files.length === 1 ? opts.title : void 0);
19300
19307
  const result = await this.uploadFile(plan, opts);
19301
19308
  successes.push(result);
@@ -19338,10 +19345,19 @@ var RecappiApiClient = class {
19338
19345
  async uploadFile(plan, opts) {
19339
19346
  const relative = plan.filePath;
19340
19347
  opts.onEvent?.({
19341
- type: "started",
19348
+ type: "progress",
19342
19349
  command: "upload",
19343
19350
  filePath: relative,
19344
- message: `Preparing ${relative}`
19351
+ status: "starting_upload",
19352
+ message: `Starting upload ${relative}`,
19353
+ data: {
19354
+ file: {
19355
+ title: plan.title,
19356
+ contentType: plan.contentType,
19357
+ sizeBytes: plan.sizeBytes,
19358
+ ...plan.durationMs ? { durationMs: plan.durationMs } : {}
19359
+ }
19360
+ }
19345
19361
  });
19346
19362
  const init = await this.postJson("/api/recordings", {
19347
19363
  title: plan.title,
@@ -20219,13 +20235,240 @@ ${lines.join("\n")}
20219
20235
  ` : "";
20220
20236
  }
20221
20237
 
20238
+ // src/progressStepper.ts
20239
+ var STEP_DEFS = [
20240
+ { key: "check", label: "Check" },
20241
+ { key: "upload", label: "Upload" },
20242
+ { key: "transcribe", label: "Transcribe" },
20243
+ { key: "done", label: "Done" }
20244
+ ];
20245
+ var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
20246
+ var BAR_WIDTH = 16;
20247
+ var LABEL_WIDTH = 11;
20248
+ function createStepperModel() {
20249
+ return {
20250
+ steps: STEP_DEFS.map((d) => ({ ...d, status: "pending", detail: "" })),
20251
+ finished: false
20252
+ };
20253
+ }
20254
+ function isStepperEvent(event) {
20255
+ if (event.command !== "upload") return false;
20256
+ if (event.type === "started") return true;
20257
+ return typeof event.status === "string";
20258
+ }
20259
+ function cloneModel(model) {
20260
+ return {
20261
+ steps: model.steps.map((s) => ({ ...s })),
20262
+ ...model.transcribeStartMs != null ? { transcribeStartMs: model.transcribeStartMs } : {},
20263
+ finished: model.finished
20264
+ };
20265
+ }
20266
+ function applyStepperEvent(model, event, nowMs) {
20267
+ const next = cloneModel(model);
20268
+ const step = (key) => next.steps.find((s) => s.key === key);
20269
+ const markDone = (key) => {
20270
+ const s = step(key);
20271
+ if (s.status !== "failed") {
20272
+ s.status = "done";
20273
+ s.percent = void 0;
20274
+ }
20275
+ };
20276
+ const startTranscribeClock = () => {
20277
+ if (next.transcribeStartMs == null) next.transcribeStartMs = nowMs;
20278
+ };
20279
+ switch (event.status) {
20280
+ case "checking_audio":
20281
+ step("check").status = "active";
20282
+ break;
20283
+ case "starting_upload": {
20284
+ markDone("check");
20285
+ const file2 = fileFromEvent(event);
20286
+ if (file2) step("check").detail = file2;
20287
+ step("upload").status = "active";
20288
+ step("upload").detail = "Starting\u2026";
20289
+ break;
20290
+ }
20291
+ case "uploading":
20292
+ step("upload").status = "active";
20293
+ step("upload").detail = "";
20294
+ if (typeof event.percent === "number") step("upload").percent = event.percent;
20295
+ break;
20296
+ case "finishing_upload":
20297
+ step("upload").status = "active";
20298
+ step("upload").detail = "Finalizing";
20299
+ step("upload").percent = 100;
20300
+ break;
20301
+ case "uploaded":
20302
+ markDone("upload");
20303
+ step("upload").detail = recordingUrl(event) ?? "Uploaded";
20304
+ break;
20305
+ case "starting_transcription":
20306
+ step("transcribe").status = "active";
20307
+ step("transcribe").detail = "Starting\u2026";
20308
+ startTranscribeClock();
20309
+ break;
20310
+ case "queued":
20311
+ step("transcribe").status = "active";
20312
+ step("transcribe").detail = "Queued";
20313
+ startTranscribeClock();
20314
+ break;
20315
+ case "running":
20316
+ step("transcribe").status = "active";
20317
+ startTranscribeClock();
20318
+ if (typeof event.percent === "number") {
20319
+ step("transcribe").percent = event.percent;
20320
+ step("transcribe").detail = "";
20321
+ } else {
20322
+ step("transcribe").percent = void 0;
20323
+ step("transcribe").detail = "Transcribing\u2026";
20324
+ }
20325
+ break;
20326
+ case "succeeded":
20327
+ markDone("transcribe");
20328
+ markDone("done");
20329
+ if (event.transcriptId) {
20330
+ step("done").detail = `\u2192 recappi transcript get ${event.transcriptId}`;
20331
+ }
20332
+ next.finished = true;
20333
+ break;
20334
+ case "failed":
20335
+ markActiveFailed(next, event.message);
20336
+ break;
20337
+ default:
20338
+ if (event.type === "started") step("check").status = "active";
20339
+ break;
20340
+ }
20341
+ return next;
20342
+ }
20343
+ function markStepperFailed(model, message) {
20344
+ const next = cloneModel(model);
20345
+ markActiveFailed(next, message);
20346
+ return next;
20347
+ }
20348
+ function completeStepperModel(model, next) {
20349
+ const m = cloneModel(model);
20350
+ const transcribe = m.steps.find((s) => s.key === "transcribe");
20351
+ if (transcribe.status === "pending") {
20352
+ transcribe.status = "skipped";
20353
+ transcribe.detail = "no --transcribe";
20354
+ }
20355
+ for (const s of m.steps) {
20356
+ if (s.status === "active" || s.status === "pending") s.status = "done";
20357
+ s.percent = void 0;
20358
+ }
20359
+ const done = m.steps.find((s) => s.key === "done");
20360
+ if (next?.transcriptId) done.detail = `\u2192 recappi transcript get ${next.transcriptId}`;
20361
+ else if (next?.recordingId) done.detail = `\u2192 recappi recordings get ${next.recordingId}`;
20362
+ m.finished = true;
20363
+ return m;
20364
+ }
20365
+ function markActiveFailed(model, message) {
20366
+ const active = model.steps.find((s) => s.status === "active");
20367
+ const target = active ?? model.steps.find((s) => s.status === "pending");
20368
+ if (target) {
20369
+ target.status = "failed";
20370
+ if (message) target.detail = message;
20371
+ }
20372
+ model.finished = true;
20373
+ }
20374
+ function formatStepperLines(model, nowMs, spinnerFrame, color) {
20375
+ return model.steps.map((s) => {
20376
+ const marker = markerFor(s, spinnerFrame);
20377
+ const label = s.label.padEnd(LABEL_WIDTH);
20378
+ let detail = s.detail;
20379
+ if (s.status === "active" && typeof s.percent === "number") {
20380
+ detail = `${progressBar(s.percent)} ${clampPercent2(s.percent)}%`;
20381
+ }
20382
+ if (s.key === "transcribe" && s.status === "active" && model.transcribeStartMs != null) {
20383
+ const elapsed = formatElapsed(Math.max(0, nowMs - model.transcribeStartMs));
20384
+ detail = detail ? `${detail} \xB7 ${elapsed}` : elapsed;
20385
+ }
20386
+ const body = ` ${marker} ${label}${detail}`.trimEnd();
20387
+ return color ? colorize(s.status, marker, body) : body;
20388
+ });
20389
+ }
20390
+ function markerFor(step, spinnerFrame) {
20391
+ switch (step.status) {
20392
+ case "done":
20393
+ return "\u2713";
20394
+ case "failed":
20395
+ return "\u2717";
20396
+ case "active":
20397
+ return SPINNER[spinnerFrame % SPINNER.length];
20398
+ case "skipped":
20399
+ return "\xB7";
20400
+ default:
20401
+ return "\u25CB";
20402
+ }
20403
+ }
20404
+ function colorize(status, marker, line) {
20405
+ const code = status === "done" ? "32" : status === "failed" ? "31" : status === "active" ? "36" : "90";
20406
+ const colored = `\x1B[${code}m${marker}\x1B[0m`;
20407
+ return line.replace(marker, colored);
20408
+ }
20409
+ function progressBar(percent) {
20410
+ const p = clampPercent2(percent);
20411
+ const filled = Math.round(p / 100 * BAR_WIDTH);
20412
+ return "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
20413
+ }
20414
+ function clampPercent2(percent) {
20415
+ return Math.max(0, Math.min(100, Math.round(percent)));
20416
+ }
20417
+ function fileFromEvent(event) {
20418
+ const data = event.data;
20419
+ if (!data || typeof data !== "object") return void 0;
20420
+ const file2 = data.file;
20421
+ if (!file2 || typeof file2 !== "object") return void 0;
20422
+ const f = file2;
20423
+ const parts = [];
20424
+ if (typeof f.title === "string" && f.title) parts.push(f.title);
20425
+ if (typeof f.sizeBytes === "number") parts.push(formatBytes(f.sizeBytes));
20426
+ if (typeof f.durationMs === "number") parts.push(formatMediaDuration(f.durationMs));
20427
+ return parts.length ? parts.join(" \xB7 ") : void 0;
20428
+ }
20429
+ function recordingUrl(event) {
20430
+ if (typeof event.message === "string" && event.message.includes("/recordings/")) {
20431
+ const idx = event.message.indexOf("http");
20432
+ if (idx >= 0) return event.message.slice(idx);
20433
+ }
20434
+ if (event.origin && event.recordingId) {
20435
+ return `${event.origin.replace(/\/+$/, "")}/recordings/${event.recordingId}`;
20436
+ }
20437
+ return void 0;
20438
+ }
20439
+ function formatBytes(bytes) {
20440
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
20441
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
20442
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
20443
+ return `${bytes} B`;
20444
+ }
20445
+ function formatMediaDuration(ms) {
20446
+ const totalSec = Math.round(ms / 1e3);
20447
+ const h = Math.floor(totalSec / 3600);
20448
+ const m = Math.floor(totalSec % 3600 / 60);
20449
+ const s = totalSec % 60;
20450
+ if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
20451
+ return `${m}:${String(s).padStart(2, "0")}`;
20452
+ }
20453
+ function formatElapsed(ms) {
20454
+ const totalSec = Math.floor(ms / 1e3);
20455
+ const h = Math.floor(totalSec / 3600);
20456
+ const m = Math.floor(totalSec % 3600 / 60);
20457
+ const s = totalSec % 60;
20458
+ const clock = h > 0 ? `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}` : `${m}:${String(s).padStart(2, "0")}`;
20459
+ return `${clock} elapsed`;
20460
+ }
20461
+
20222
20462
  // src/render.ts
20223
20463
  function createHumanProgressState(interactive) {
20224
20464
  return {
20225
20465
  interactive,
20226
20466
  activeLineLength: 0,
20227
20467
  lastLineByScope: /* @__PURE__ */ new Map(),
20228
- lastUploadBucketByScope: /* @__PURE__ */ new Map()
20468
+ lastUploadBucketByScope: /* @__PURE__ */ new Map(),
20469
+ stepperLinesDrawn: 0,
20470
+ spinnerFrame: 0,
20471
+ stepperShown: false
20229
20472
  };
20230
20473
  }
20231
20474
  function renderSuccess(command, data, opts) {
@@ -20247,6 +20490,7 @@ function renderSuccess(command, data, opts) {
20247
20490
  renderEnvelope(envelope, opts);
20248
20491
  return;
20249
20492
  }
20493
+ finalizeStepperSuccess(command, filtered, opts);
20250
20494
  finishHumanProgress(opts);
20251
20495
  renderHumanSuccess(command, filtered, opts);
20252
20496
  }
@@ -20275,6 +20519,7 @@ function renderFailure(command, error51, opts, data) {
20275
20519
  renderEnvelope(envelope, opts);
20276
20520
  return;
20277
20521
  }
20522
+ if (finalizeStepperFailure(command, error51, opts, data)) return;
20278
20523
  finishHumanProgress(opts);
20279
20524
  opts.stderr(`recappi: ${error51.message}
20280
20525
  `);
@@ -20300,6 +20545,10 @@ function renderEvent(event, opts) {
20300
20545
  return;
20301
20546
  }
20302
20547
  if ((event.type === "started" || event.type === "progress") && opts.mode === "human") {
20548
+ if (opts.progress?.interactive && isStepperEvent(event)) {
20549
+ renderStepperEvent(event, opts);
20550
+ return;
20551
+ }
20303
20552
  const line = formatHumanProgress(event, opts);
20304
20553
  if (line) {
20305
20554
  if (isPersistentProgressEvent(event)) writePersistentHumanProgress(line, opts);
@@ -20307,6 +20556,56 @@ function renderEvent(event, opts) {
20307
20556
  }
20308
20557
  }
20309
20558
  }
20559
+ function renderStepperEvent(event, opts) {
20560
+ const state = opts.progress;
20561
+ if (!state) return;
20562
+ if (!state.stepper) state.stepper = createStepperModel();
20563
+ state.stepper = applyStepperEvent(state.stepper, event, Date.now());
20564
+ state.spinnerFrame += 1;
20565
+ drawStepperBlock(state, opts);
20566
+ }
20567
+ function drawStepperBlock(state, opts) {
20568
+ if (!state.stepper) return;
20569
+ const lines = formatStepperLines(state.stepper, Date.now(), state.spinnerFrame, true);
20570
+ let out = "";
20571
+ if (state.stepperLinesDrawn > 0) out += `\x1B[${state.stepperLinesDrawn}A`;
20572
+ for (const line of lines) out += `\x1B[2K${line}
20573
+ `;
20574
+ opts.stderr(out);
20575
+ state.stepperLinesDrawn = lines.length;
20576
+ state.stepperShown = true;
20577
+ }
20578
+ function finalizeStepperSuccess(command, data, opts) {
20579
+ const state = opts.progress;
20580
+ if (!state?.stepper || state.stepperLinesDrawn === 0 || command !== "upload") return;
20581
+ const success2 = firstUploadSuccess(data);
20582
+ state.stepper = completeStepperModel(state.stepper, success2);
20583
+ drawStepperBlock(state, opts);
20584
+ }
20585
+ function firstUploadSuccess(data) {
20586
+ if (!isUploadBatch(data) || data.successes.length === 0) return void 0;
20587
+ const s = data.successes[0];
20588
+ return {
20589
+ ...s.transcriptId ? { transcriptId: s.transcriptId } : {},
20590
+ ...s.recordingId ? { recordingId: s.recordingId } : {}
20591
+ };
20592
+ }
20593
+ function finalizeStepperFailure(command, error51, opts, data) {
20594
+ const state = opts.progress;
20595
+ if (!state?.stepper || state.stepperLinesDrawn === 0) return false;
20596
+ state.stepper = markStepperFailed(state.stepper, stepperFailureDetail(command, error51, data));
20597
+ drawStepperBlock(state, opts);
20598
+ state.stepperLinesDrawn = 0;
20599
+ state.stepper = void 0;
20600
+ return true;
20601
+ }
20602
+ function stepperFailureDetail(command, error51, data) {
20603
+ if (command === "upload" && isUploadBatch(data) && data.failures.length > 0) {
20604
+ const first = data.failures[0].error;
20605
+ return `${first.message} (${first.code})`;
20606
+ }
20607
+ return `${error51.message} (${error51.code})`;
20608
+ }
20310
20609
  function renderEnvelope(envelope, opts) {
20311
20610
  const parsed = cliEnvelopeSchema.parse(envelope);
20312
20611
  opts.stdout(`${stableStringify(parsed, opts.compact === true)}
@@ -20356,7 +20655,7 @@ function renderHumanSuccess(command, data, opts) {
20356
20655
  }
20357
20656
  if (typeof billing.storageBytes === "number") {
20358
20657
  const cap = formatNullableCap(billing.storageCapBytes, "bytes");
20359
- opts.stdout(` storage: ${formatBytes(billing.storageBytes)} / ${cap}
20658
+ opts.stdout(` storage: ${formatBytes2(billing.storageBytes)} / ${cap}
20360
20659
  `);
20361
20660
  }
20362
20661
  const localStore = isRecord4(data.localStore) ? data.localStore : {};
@@ -20418,7 +20717,7 @@ Next cursor: ${data.nextCursor}
20418
20717
  if (typeof data.durationMs === "number")
20419
20718
  opts.stdout(` duration: ${formatDurationMs(data.durationMs)}
20420
20719
  `);
20421
- if (typeof data.sizeBytes === "number") opts.stdout(` size: ${formatBytes(data.sizeBytes)}
20720
+ if (typeof data.sizeBytes === "number") opts.stdout(` size: ${formatBytes2(data.sizeBytes)}
20422
20721
  `);
20423
20722
  if (typeof data.activeTranscriptId === "string") {
20424
20723
  opts.stdout(` activeTranscriptId: ${data.activeTranscriptId}
@@ -20467,6 +20766,7 @@ Next:
20467
20766
  return;
20468
20767
  }
20469
20768
  if (command === "upload" && isUploadBatch(data)) {
20769
+ if (opts.progress?.stepperShown && data.failures.length === 0) return;
20470
20770
  if (data.successes.length > 0) {
20471
20771
  opts.stdout(uploadSuccessHeading(data.successes));
20472
20772
  }
@@ -20622,8 +20922,7 @@ function formatHumanProgress(event, opts) {
20622
20922
  const scope = progressScope(event);
20623
20923
  let line;
20624
20924
  if (event.type === "started") {
20625
- const label = humanFileLabel(event.filePath);
20626
- line = label ? `Preparing ${label}` : "Preparing upload";
20925
+ line = event.message;
20627
20926
  } else if (event.command === "upload") {
20628
20927
  line = formatUploadProgress(event, opts, scope);
20629
20928
  } else if (event.command === "jobs wait") {
@@ -20639,13 +20938,20 @@ function formatHumanProgress(event, opts) {
20639
20938
  return line;
20640
20939
  }
20641
20940
  function formatUploadProgress(event, opts, scope) {
20941
+ const label = event.filePath ? humanFileLabel(event.filePath) : void 0;
20942
+ if (event.status === "checking_audio") {
20943
+ return label ? `Checking ${label}` : "Checking audio file";
20944
+ }
20945
+ if (event.status === "starting_upload") {
20946
+ return label ? `Starting upload ${label}` : "Starting upload";
20947
+ }
20642
20948
  if (event.status === "uploading" && typeof event.percent === "number") {
20643
20949
  const state = opts.progress;
20644
20950
  const bucket = Math.min(100, Math.max(0, Math.floor(event.percent / 10) * 10));
20645
20951
  const previous = state?.lastUploadBucketByScope.get(scope);
20646
20952
  if (previous !== void 0 && bucket <= previous && bucket !== 100) return void 0;
20647
20953
  state?.lastUploadBucketByScope.set(scope, bucket);
20648
- return `Uploading${event.filePath ? ` ${humanFileLabel(event.filePath)}` : ""}: ${event.percent}%`;
20954
+ return `Uploading${label ? ` ${label}` : ""}: ${event.percent}%`;
20649
20955
  }
20650
20956
  if (event.status === "finishing_upload") return "Finalizing upload";
20651
20957
  if (event.status === "uploaded") {
@@ -20686,7 +20992,13 @@ function writePersistentHumanProgress(line, opts) {
20686
20992
  }
20687
20993
  function finishHumanProgress(opts) {
20688
20994
  const state = opts.progress;
20689
- if (!state?.interactive || state.activeLineLength === 0) return;
20995
+ if (!state) return;
20996
+ if (state.stepperLinesDrawn > 0) {
20997
+ state.stepperLinesDrawn = 0;
20998
+ state.stepper = void 0;
20999
+ return;
21000
+ }
21001
+ if (!state.interactive || state.activeLineLength === 0) return;
20690
21002
  opts.stderr("\n");
20691
21003
  state.activeLineLength = 0;
20692
21004
  }
@@ -20779,7 +21091,7 @@ function numberText(value) {
20779
21091
  function formatDurationMs(ms) {
20780
21092
  return formatClock(ms / 1e3);
20781
21093
  }
20782
- function formatBytes(bytes) {
21094
+ function formatBytes2(bytes) {
20783
21095
  if (bytes < 1024) return `${bytes} B`;
20784
21096
  const units = ["KB", "MB", "GB", "TB"];
20785
21097
  let value = bytes / 1024;
@@ -20794,7 +21106,7 @@ function formatBytes(bytes) {
20794
21106
  function formatNullableCap(value, unit) {
20795
21107
  if (value === null || value === void 0) return "Unlimited";
20796
21108
  if (typeof value !== "number" || !Number.isFinite(value)) return "Unlimited";
20797
- return unit === "bytes" ? formatBytes(value) : String(value);
21109
+ return unit === "bytes" ? formatBytes2(value) : String(value);
20798
21110
  }
20799
21111
  function formatClock(seconds) {
20800
21112
  const total = Math.max(0, Math.floor(seconds));
@@ -21453,7 +21765,7 @@ function RecordingHeroScreen({
21453
21765
  const phase = stoppedPhase(artifact);
21454
21766
  const meta3 = [
21455
21767
  telemetry.durationMs != null ? formatClockMs(telemetry.durationMs) : null,
21456
- formatBytes2(telemetry.sizeBytes) || null
21768
+ formatBytes3(telemetry.sizeBytes) || null
21457
21769
  ].filter(Boolean).join(" \xB7 ");
21458
21770
  const saved = artifact?.uploadStatus === "uploaded" ? "\u2713 Saved to Recappi Cloud" : "\u2713 Saved to your Mac";
21459
21771
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingX: 1, children: [
@@ -21488,7 +21800,7 @@ function RecordingHeroScreen({
21488
21800
  const starting = telemetry.status === "starting" || telemetry.status === "stopping";
21489
21801
  const badge = paused ? "\u23F8 PAUSED" : starting ? "\u2026" : "\u23FA REC";
21490
21802
  const meterW = Math.max(10, Math.min(72, innerWidth - 20));
21491
- const sizeStr = telemetry.sizeBytes ? formatBytes2(telemetry.sizeBytes) : "";
21803
+ const sizeStr = telemetry.sizeBytes ? formatBytes3(telemetry.sizeBytes) : "";
21492
21804
  const context = [telemetry.sourceLabel, telemetry.micEnabled ? "Microphone" : null, sizeStr || null].filter(Boolean).join(" \xB7 ");
21493
21805
  const waveRows = waveRowsFor(size.rows);
21494
21806
  const meterBlockRows = (telemetry.micEnabled ? 2 : 1) * (waveRows + 1) + (telemetry.micEnabled ? 1 : 0);