recappi 0.1.79 → 0.1.81

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
@@ -1288,7 +1288,8 @@ function RecordingDetailView({
1288
1288
  item,
1289
1289
  nowMs,
1290
1290
  transcript,
1291
- audio
1291
+ audio,
1292
+ localDir
1292
1293
  }) {
1293
1294
  const size = useTerminalSize();
1294
1295
  const [tab, setTab] = useState4("summary");
@@ -1355,6 +1356,10 @@ function RecordingDetailView({
1355
1356
  ] }),
1356
1357
  meta3 ? /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: meta3 }) : null,
1357
1358
  /* @__PURE__ */ jsx14(AudioActionRow, { item, audio }),
1359
+ localDir ? /* @__PURE__ */ jsxs11(Text12, { children: [
1360
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "\u2302 Local \xB7 " }),
1361
+ /* @__PURE__ */ jsx14(Text12, { dimColor: true, wrap: "truncate-middle", children: localDir })
1362
+ ] }) : null,
1358
1363
  !item.activeTranscriptId ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Transcript not available yet" }) }) : transcript === "loading" || transcript === void 0 ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "Loading\u2026" }) }) : transcript === "error" ? /* @__PURE__ */ jsx14(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text12, { dimColor: true, children: "(transcript unavailable)" }) }) : /* @__PURE__ */ jsxs11(Fragment5, { children: [
1359
1364
  /* @__PURE__ */ jsx14(TabBar, { active: tab }),
1360
1365
  /* @__PURE__ */ jsx14(Box12, { marginTop: 1, flexDirection: "column", children: tab === "summary" ? /* @__PURE__ */ jsx14(SummaryPane, { summary, budget: paneBudget }) : tab === "chapters" ? /* @__PURE__ */ jsx14(ChaptersPane, { chapters, win: chapWin, selectedIndex: chapterSel }) : /* @__PURE__ */ jsx14(TranscriptPane, { segments, win: segWin }) })
@@ -1365,6 +1370,7 @@ function RecordingDetailView({
1365
1370
  scrollable ? " \xB7 \u2191\u2193 scroll" : "",
1366
1371
  ready ? " \xB7 " : "",
1367
1372
  `o open \xB7 d download \xB7 f finder`,
1373
+ localDir ? " \xB7 l folder" : "",
1368
1374
  " \xB7 T re-transcribe \xB7 s re-summarize \xB7 a ask \xB7 e export",
1369
1375
  item.activeTranscriptId ? " \xB7 t full" : "",
1370
1376
  links.webUrl ? " \xB7 w web" : "",
@@ -1487,12 +1493,14 @@ function AskScreen({
1487
1493
  onOpenTranscript,
1488
1494
  spinnerFrame
1489
1495
  }) {
1496
+ const size = useTerminalSize();
1490
1497
  const [phase, setPhase] = useState5("input");
1491
1498
  const [question, setQuestion] = useState5("");
1492
1499
  const [asked, setAsked] = useState5("");
1493
1500
  const [content, setContent] = useState5("");
1494
1501
  const [citations, setCitations] = useState5([]);
1495
1502
  const [error51, setError] = useState5(void 0);
1503
+ const [scroll, setScroll] = useState5(0);
1496
1504
  const runIdRef = useRef2(0);
1497
1505
  const submit = useCallback(
1498
1506
  (raw) => {
@@ -1504,6 +1512,7 @@ function AskScreen({
1504
1512
  setContent("");
1505
1513
  setCitations([]);
1506
1514
  setError(void 0);
1515
+ setScroll(0);
1507
1516
  void (async () => {
1508
1517
  try {
1509
1518
  let text = "";
@@ -1546,7 +1555,16 @@ function AskScreen({
1546
1555
  setContent("");
1547
1556
  setCitations([]);
1548
1557
  setError(void 0);
1558
+ setScroll(0);
1549
1559
  };
1560
+ const answerText = phase === "done" ? formatAskAnswerPlain(content, citations) : content ? strippedAskAnswer(content) : "";
1561
+ const innerWidth = Math.max(10, size.columns - 2);
1562
+ const lines = answerText ? wrapToLines(answerText, innerWidth) : [];
1563
+ const paneBudget = Math.max(3, size.rows - 9);
1564
+ const maxScroll = Math.max(0, lines.length - paneBudget);
1565
+ const effectiveScroll = phase === "asking" ? maxScroll : Math.min(scroll, maxScroll);
1566
+ const windowLines = lines.slice(effectiveScroll, effectiveScroll + paneBudget);
1567
+ const page = Math.max(1, paneBudget - 1);
1550
1568
  useInput5((input, key) => {
1551
1569
  if (key.escape) {
1552
1570
  runIdRef.current++;
@@ -1559,10 +1577,14 @@ function AskScreen({
1559
1577
  if (input && !key.ctrl && !key.meta) setQuestion((q) => q + input);
1560
1578
  return;
1561
1579
  }
1562
- if (phase === "done" || phase === "error") {
1563
- if (input === "a") return reset();
1564
- if (input === "t" && onOpenTranscript) return onOpenTranscript();
1565
- }
1580
+ if (key.downArrow || input === "j") setScroll((s) => Math.min(maxScroll, s + 1));
1581
+ else if (key.upArrow || input === "k") setScroll((s) => Math.max(0, s - 1));
1582
+ else if (key.pageDown || input === " ") setScroll((s) => Math.min(maxScroll, s + page));
1583
+ else if (key.pageUp || input === "b") setScroll((s) => Math.max(0, s - page));
1584
+ else if (input === "g") setScroll(0);
1585
+ else if (input === "G") setScroll(maxScroll);
1586
+ else if (input === "a") reset();
1587
+ else if (input === "t" && onOpenTranscript) onOpenTranscript();
1566
1588
  });
1567
1589
  return /* @__PURE__ */ jsxs12(Box13, { flexDirection: "column", paddingX: 1, children: [
1568
1590
  /* @__PURE__ */ jsxs12(Text13, { dimColor: true, children: [
@@ -1578,63 +1600,56 @@ function AskScreen({
1578
1600
  /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: "Type a question \xB7 \u23CE ask \xB7 esc back" }) })
1579
1601
  ] }) : /* @__PURE__ */ jsxs12(Box13, { marginTop: 1, flexDirection: "column", children: [
1580
1602
  /* @__PURE__ */ jsx15(Text13, { dimColor: true, wrap: "truncate-end", children: `? ${asked}` }),
1581
- /* @__PURE__ */ jsx15(Box13, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx15(AnswerBody, { phase, content, citations, error: error51, spinnerFrame }) }),
1582
- phase === "done" ? /* @__PURE__ */ jsx15(Sources, { citations }) : null,
1603
+ /* @__PURE__ */ jsx15(Box13, { marginTop: 1, flexDirection: "column", children: phase === "asking" && !content ? /* @__PURE__ */ jsx15(Text13, { color: "cyan", children: `${SPINNER2[spinnerFrame % SPINNER2.length]} Thinking\u2026` }) : windowLines.map((line, i) => /* @__PURE__ */ jsx15(AnswerLine, { line }, effectiveScroll + i)) }),
1604
+ maxScroll > 0 ? /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: ` ${Math.min(effectiveScroll + paneBudget, lines.length)} / ${lines.length}${phase === "asking" ? " \xB7 streaming" : " \xB7 \u2191\u2193 scroll"}` }) : null,
1605
+ phase === "error" ? /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx15(Text13, { color: "red", children: error51 ? `Ask failed: ${error51}` : "Ask failed" }) }) : null,
1583
1606
  /* @__PURE__ */ jsx15(Box13, { marginTop: 1, children: /* @__PURE__ */ jsxs12(Text13, { dimColor: true, children: [
1584
1607
  phase === "asking" ? "esc cancel" : "a ask again",
1585
1608
  (phase === "done" || phase === "error") && onOpenTranscript ? " \xB7 t transcript" : "",
1609
+ maxScroll > 0 ? " \xB7 \u2191\u2193 scroll" : "",
1586
1610
  phase !== "asking" ? " \xB7 esc back" : ""
1587
1611
  ] }) })
1588
1612
  ] })
1589
1613
  ] });
1590
1614
  }
1591
- function AnswerBody({
1592
- phase,
1593
- content,
1594
- citations,
1595
- error: error51,
1596
- spinnerFrame
1597
- }) {
1598
- if (phase === "error") {
1599
- return /* @__PURE__ */ jsx15(Text13, { color: "red", children: error51 ? `Ask failed: ${error51}` : "Ask failed" });
1600
- }
1601
- if (phase === "asking" && !content) {
1602
- return /* @__PURE__ */ jsx15(Text13, { color: "cyan", children: `${SPINNER2[spinnerFrame % SPINNER2.length]} Thinking\u2026` });
1603
- }
1604
- if (phase === "asking") {
1605
- return /* @__PURE__ */ jsx15(Text13, { children: strippedAskAnswer(content) });
1606
- }
1607
- const segments = renderAskInline(content, citations);
1608
- return /* @__PURE__ */ jsx15(Text13, { children: segments.map(
1609
- (segment, i) => segment.kind === "text" ? /* @__PURE__ */ jsx15(Text13, { children: segment.text }, i) : /* @__PURE__ */ jsx15(Text13, { color: "cyan", children: `\u27E8${segment.label}\u27E9` }, i)
1615
+ function AnswerLine({ line }) {
1616
+ if (line === "") return /* @__PURE__ */ jsx15(Text13, { children: " " });
1617
+ const parts = line.split(/(⟨[^⟩]*⟩)/);
1618
+ return /* @__PURE__ */ jsx15(Text13, { children: parts.map(
1619
+ (part, i) => part.startsWith("\u27E8") && part.endsWith("\u27E9") ? /* @__PURE__ */ jsx15(Text13, { color: "cyan", children: part }, i) : /* @__PURE__ */ jsx15(Text13, { children: part }, i)
1610
1620
  ) });
1611
1621
  }
1612
- function Sources({ citations }) {
1613
- const seen = /* @__PURE__ */ new Set();
1614
- const unique = [];
1615
- for (const citation of citations) {
1616
- const key = citation.segmentId ?? askCitationInlineLabel(citation);
1617
- if (seen.has(key)) continue;
1618
- seen.add(key);
1619
- unique.push(citation);
1620
- }
1621
- if (unique.length === 0) return null;
1622
- return /* @__PURE__ */ jsxs12(Box13, { marginTop: 1, flexDirection: "column", children: [
1623
- /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: "Sources" }),
1624
- unique.map((citation, i) => {
1625
- const meta3 = [citation.speaker?.trim(), citation.snippet?.trim()].filter(Boolean).join(" \xB7 ");
1626
- return /* @__PURE__ */ jsxs12(Text13, { wrap: "truncate-end", children: [
1627
- /* @__PURE__ */ jsx15(Text13, { color: "cyan", children: `\u27E8${askCitationInlineLabel(citation)}\u27E9` }),
1628
- meta3 ? /* @__PURE__ */ jsx15(Text13, { dimColor: true, children: ` ${meta3}` }) : null
1629
- ] }, i);
1630
- })
1631
- ] });
1622
+ function wrapToLines(text, width) {
1623
+ const out = [];
1624
+ for (const paragraph of text.split("\n")) {
1625
+ if (paragraph === "") {
1626
+ out.push("");
1627
+ continue;
1628
+ }
1629
+ let current = "";
1630
+ let currentWidth = 0;
1631
+ for (const ch of paragraph) {
1632
+ const w = displayWidth(ch);
1633
+ if (current !== "" && currentWidth + w > width) {
1634
+ out.push(current);
1635
+ current = ch;
1636
+ currentWidth = w;
1637
+ } else {
1638
+ current += ch;
1639
+ currentWidth += w;
1640
+ }
1641
+ }
1642
+ out.push(current);
1643
+ }
1644
+ return out;
1632
1645
  }
1633
1646
  var SPINNER2;
1634
1647
  var init_AskScreen = __esm({
1635
1648
  "src/tui/AskScreen.tsx"() {
1636
1649
  "use strict";
1637
1650
  init_askInline();
1651
+ init_format();
1652
+ init_terminal();
1638
1653
  SPINNER2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
1639
1654
  }
1640
1655
  });
@@ -2769,6 +2784,13 @@ function AppShell({
2769
2784
  },
2770
2785
  [onResummarize, refresh, recordings, now, refetchTranscript]
2771
2786
  );
2787
+ const [sessionDirByRecording, setSessionDirByRecording] = useState9(
2788
+ () => /* @__PURE__ */ new Map()
2789
+ );
2790
+ const rememberSessionDir = useCallback2((recordingId, dir) => {
2791
+ if (!dir) return;
2792
+ setSessionDirByRecording((m) => m.get(recordingId) === dir ? m : new Map(m).set(recordingId, dir));
2793
+ }, []);
2772
2794
  const syncedTextRef = useRef4(/* @__PURE__ */ new Set());
2773
2795
  const syncRecordingText2 = useCallback2(
2774
2796
  async (recordingId, opts = {}) => {
@@ -2777,12 +2799,13 @@ function AppShell({
2777
2799
  try {
2778
2800
  const data = await onSyncRecordingText(recordingId);
2779
2801
  syncedTextRef.current.add(recordingId);
2802
+ rememberSessionDir(recordingId, data.sessionDir);
2780
2803
  if (opts.manual) setNotice(`Text synced \xB7 ${data.sessionDir}`);
2781
2804
  } catch (error51) {
2782
2805
  if (opts.manual) setNotice(transcribeHandoffErrorCopy(error51));
2783
2806
  }
2784
2807
  },
2785
- [onSyncRecordingText]
2808
+ [onSyncRecordingText, rememberSessionDir]
2786
2809
  );
2787
2810
  const syncRecordingAudio2 = useCallback2(
2788
2811
  async (recordingId) => {
@@ -2793,12 +2816,33 @@ function AppShell({
2793
2816
  setNotice("Downloading audio\u2026");
2794
2817
  try {
2795
2818
  const data = await onSyncRecordingAudio(recordingId);
2819
+ rememberSessionDir(recordingId, data.sessionDir);
2796
2820
  setNotice(`Audio saved \xB7 ${data.audioPath}`);
2797
2821
  } catch (error51) {
2798
2822
  setNotice(transcribeHandoffErrorCopy(error51));
2799
2823
  }
2800
2824
  },
2801
- [onSyncRecordingAudio]
2825
+ [onSyncRecordingAudio, rememberSessionDir]
2826
+ );
2827
+ const openLocalFolder = useCallback2(
2828
+ async (recordingId) => {
2829
+ const dir = sessionDirByRecording.get(recordingId);
2830
+ if (!dir) {
2831
+ setNotice("No local copy yet \u2014 it syncs when you open the recording.");
2832
+ return;
2833
+ }
2834
+ if (!recordingAudio) {
2835
+ setNotice("Opening the local folder isn't available in this CLI session.");
2836
+ return;
2837
+ }
2838
+ try {
2839
+ await recordingAudio.openPath(dir);
2840
+ setNotice(`Opened ${dir}`);
2841
+ } catch (error51) {
2842
+ setNotice(transcribeHandoffErrorCopy(error51));
2843
+ }
2844
+ },
2845
+ [sessionDirByRecording, recordingAudio]
2802
2846
  );
2803
2847
  const exportRecordingForAgent = useCallback2(
2804
2848
  async (recordingId) => {
@@ -3125,6 +3169,7 @@ function AppShell({
3125
3169
  if (onSyncRecordingAudio) void syncRecordingAudio2(rec.recordingId);
3126
3170
  else void runAudio(rec.recordingId, "download");
3127
3171
  } else if (input === "f" && rec) void runAudio(rec.recordingId, "finder");
3172
+ else if (input === "l" && rec) void openLocalFolder(rec.recordingId);
3128
3173
  else if (input === "w" && links.webUrl) openUrl2?.(links.webUrl);
3129
3174
  else if (input === "c" && links.webUrl) {
3130
3175
  copyText2?.(links.webUrl);
@@ -3166,7 +3211,8 @@ function AppShell({
3166
3211
  item: rec,
3167
3212
  nowMs: now(),
3168
3213
  transcript: detailTranscript,
3169
- audio: audioCache.get(rec.recordingId)
3214
+ audio: audioCache.get(rec.recordingId),
3215
+ localDir: sessionDirByRecording.get(rec.recordingId)
3170
3216
  }
3171
3217
  ) });
3172
3218
  }