@markusylisiurunen/tau 0.3.53 → 0.3.55

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.
Files changed (118) hide show
  1. package/dist/core/commands/registry.js +10 -5
  2. package/dist/core/commands/registry.js.map +1 -1
  3. package/dist/core/config/index.js +1 -1
  4. package/dist/core/config/index.js.map +1 -1
  5. package/dist/core/config/schema.js +10 -2
  6. package/dist/core/config/schema.js.map +1 -1
  7. package/dist/core/diff_review/bridge.js +2 -1
  8. package/dist/core/diff_review/bridge.js.map +1 -1
  9. package/dist/core/diff_review/tool.js +1 -1
  10. package/dist/core/diff_review/tool.js.map +1 -1
  11. package/dist/core/static/tau_docs/config-reference.md +6 -6
  12. package/dist/core/static/tau_docs/credentials.md +3 -2
  13. package/dist/core/static/tau_docs/telegram.md +4 -3
  14. package/dist/core/static/tau_docs/troubleshooting.md +1 -1
  15. package/dist/core/static/tau_docs/tui.md +5 -3
  16. package/dist/core/telegram/adapter.js +40 -20
  17. package/dist/core/telegram/adapter.js.map +1 -1
  18. package/dist/core/telegram/cli.js +4 -2
  19. package/dist/core/telegram/cli.js.map +1 -1
  20. package/dist/core/telegram/runtime.js +1 -0
  21. package/dist/core/telegram/runtime.js.map +1 -1
  22. package/dist/core/utils/gemini_transcription.js +14 -15
  23. package/dist/core/utils/gemini_transcription.js.map +1 -1
  24. package/dist/core/utils/mistral_transcription.js +6 -2
  25. package/dist/core/utils/mistral_transcription.js.map +1 -1
  26. package/dist/core/utils/openai_transcription.js +431 -0
  27. package/dist/core/utils/openai_transcription.js.map +1 -0
  28. package/dist/core/utils/speech_to_text.js +66 -14
  29. package/dist/core/utils/speech_to_text.js.map +1 -1
  30. package/dist/core/version.js +1 -1
  31. package/dist/diff_tool/app/dist/assets/{angular-html-vSIwre92.js → angular-html-DoxI_och.js} +1 -1
  32. package/dist/diff_tool/app/dist/assets/{angular-ts-B9yWa-IT.js → angular-ts-C7CUd-D2.js} +1 -1
  33. package/dist/diff_tool/app/dist/assets/{apl-CBg9ewu1.js → apl-7dieYM-l.js} +1 -1
  34. package/dist/diff_tool/app/dist/assets/{astro-BVjxSYjj.js → astro-Czj3O4jM.js} +1 -1
  35. package/dist/diff_tool/app/dist/assets/{blade-jvqV5kiv.js → blade-ClS0E2cC.js} +1 -1
  36. package/dist/diff_tool/app/dist/assets/{c-DDAfW08c.js → c-BKvTh-Jt.js} +1 -1
  37. package/dist/diff_tool/app/dist/assets/{chapel-Qflwfgbt.js → chapel-qvFrfwEr.js} +1 -1
  38. package/dist/diff_tool/app/dist/assets/{cobol-4iJI1LUg.js → cobol-B0vsaB-J.js} +1 -1
  39. package/dist/diff_tool/app/dist/assets/{coffee-C9LbGGO3.js → coffee-BUlcrU75.js} +1 -1
  40. package/dist/diff_tool/app/dist/assets/{cpp-B-x6veTx.js → cpp-CKqEgX1l.js} +1 -1
  41. package/dist/diff_tool/app/dist/assets/{crystal-S06EUmKn.js → crystal-FCqSmIJ_.js} +1 -1
  42. package/dist/diff_tool/app/dist/assets/{css-DA4RYriG.js → css-zZvYFJFc.js} +1 -1
  43. package/dist/diff_tool/app/dist/assets/{edge-7dx6BjGp.js → edge-BKlbCo1f.js} +1 -1
  44. package/dist/diff_tool/app/dist/assets/{elixir-DPywG97a.js → elixir-B2gSxtgZ.js} +1 -1
  45. package/dist/diff_tool/app/dist/assets/{elm-Dgo_WnqC.js → elm-DFEdzPJz.js} +1 -1
  46. package/dist/diff_tool/app/dist/assets/{erb-CVW4PjFl.js → erb-DSgn8fHS.js} +1 -1
  47. package/dist/diff_tool/app/dist/assets/{git-rebase-CtU0gmBJ.js → git-rebase-CcgDECQr.js} +1 -1
  48. package/dist/diff_tool/app/dist/assets/{glimmer-js-twAYiPP2.js → glimmer-js-Dwxi4cX2.js} +1 -1
  49. package/dist/diff_tool/app/dist/assets/{glimmer-ts-BfNoXayN.js → glimmer-ts-BnJVER8W.js} +1 -1
  50. package/dist/diff_tool/app/dist/assets/{glsl-D_XrtFF7.js → glsl-z7_gU-HN.js} +1 -1
  51. package/dist/diff_tool/app/dist/assets/{graphql-B_GeapzM.js → graphql-Dt7V-d3L.js} +1 -1
  52. package/dist/diff_tool/app/dist/assets/{hack-BCaCVoSX.js → hack-jVkr6EC8.js} +1 -1
  53. package/dist/diff_tool/app/dist/assets/{haml-z9Mz7I1O.js → haml-BF7cRiOF.js} +1 -1
  54. package/dist/diff_tool/app/dist/assets/{handlebars-D0FVYagc.js → handlebars-DAhNxR3E.js} +1 -1
  55. package/dist/diff_tool/app/dist/assets/{html-BOM4tb7u.js → html-B7QW5YvU.js} +1 -1
  56. package/dist/diff_tool/app/dist/assets/{html-derivative-CsSlhJgr.js → html-derivative-B-vyaE-N.js} +1 -1
  57. package/dist/diff_tool/app/dist/assets/{http-Cjy9oaQn.js → http-Drkl04RP.js} +1 -1
  58. package/dist/diff_tool/app/dist/assets/{hurl-CjLQ7ql5.js → hurl-7gTFrvcN.js} +1 -1
  59. package/dist/diff_tool/app/dist/assets/{index-QO81yN6b.js → index-C9CiIimI.js} +41 -41
  60. package/dist/diff_tool/app/dist/assets/{java-cn8DiFD8.js → java-ZmvXuvs3.js} +1 -1
  61. package/dist/diff_tool/app/dist/assets/{javascript-CEnHZaZM.js → javascript-C-OECu_Z.js} +1 -1
  62. package/dist/diff_tool/app/dist/assets/{jinja-DIp6kVk7.js → jinja-hvkV87CG.js} +1 -1
  63. package/dist/diff_tool/app/dist/assets/{jison-Dx116ito.js → jison-eJBDxKI8.js} +1 -1
  64. package/dist/diff_tool/app/dist/assets/{json-C6B9h0n5.js → json-BPQU2w0u.js} +1 -1
  65. package/dist/diff_tool/app/dist/assets/{jsx-CDEGT2B2.js → jsx-C19s5MOj.js} +1 -1
  66. package/dist/diff_tool/app/dist/assets/{julia-CsIJA3Km.js → julia-CYISvCwi.js} +1 -1
  67. package/dist/diff_tool/app/dist/assets/{just-DZRYvkQw.js → just-5omBg5pB.js} +1 -1
  68. package/dist/diff_tool/app/dist/assets/{latex-CeR-RQOI.js → latex-B_L85F2z.js} +1 -1
  69. package/dist/diff_tool/app/dist/assets/{liquid-Dhf8N4mn.js → liquid-BxbaQPPS.js} +1 -1
  70. package/dist/diff_tool/app/dist/assets/{lua-zVTVaJzn.js → lua-DQA0z-44.js} +1 -1
  71. package/dist/diff_tool/app/dist/assets/{marko-BGPq5r3o.js → marko-Dj5RV8IH.js} +1 -1
  72. package/dist/diff_tool/app/dist/assets/{mdc-D8K78bym.js → mdc-CoHI6VJN.js} +1 -1
  73. package/dist/diff_tool/app/dist/assets/{mojo-CFuIV2fl.js → mojo-fg40_ntQ.js} +1 -1
  74. package/dist/diff_tool/app/dist/assets/{nginx-Bo-bEa75.js → nginx-COnrY-gb.js} +1 -1
  75. package/dist/diff_tool/app/dist/assets/{nim-Bfy5M54r.js → nim-Ccfp0p1j.js} +1 -1
  76. package/dist/diff_tool/app/dist/assets/{org-BKetN7sT.js → org-RForNsWm.js} +1 -1
  77. package/dist/diff_tool/app/dist/assets/{perl-WiwTOQTV.js → perl-DhHRsiUu.js} +1 -1
  78. package/dist/diff_tool/app/dist/assets/{php-CM2FzhGV.js → php-BlxKhJek.js} +1 -1
  79. package/dist/diff_tool/app/dist/assets/{pug-D2WvUHZ-.js → pug-o9YxnGzH.js} +1 -1
  80. package/dist/diff_tool/app/dist/assets/{qml-DGPbc6TL.js → qml-Dzf494vL.js} +1 -1
  81. package/dist/diff_tool/app/dist/assets/{r-CCFYJ_KM.js → r-Dp0OUrF6.js} +1 -1
  82. package/dist/diff_tool/app/dist/assets/{razor-D1JuQWQz.js → razor-gylahu-m.js} +1 -1
  83. package/dist/diff_tool/app/dist/assets/{regexp-zTiHwMow.js → regexp-C0rsDjCm.js} +1 -1
  84. package/dist/diff_tool/app/dist/assets/{rst-CuUAArfH.js → rst-D-DRAFtq.js} +1 -1
  85. package/dist/diff_tool/app/dist/assets/{ruby-DznrLCoG.js → ruby-BnY40vXq.js} +1 -1
  86. package/dist/diff_tool/app/dist/assets/{sas-D2hJmLva.js → sas-BaSo_s7i.js} +1 -1
  87. package/dist/diff_tool/app/dist/assets/{scss-Cjb3p_6n.js → scss-BX5R-Yyw.js} +1 -1
  88. package/dist/diff_tool/app/dist/assets/{shellscript-BtwI-tfX.js → shellscript-CpRFVBRj.js} +1 -1
  89. package/dist/diff_tool/app/dist/assets/{shellsession-BYirS1xP.js → shellsession-BGRUDtms.js} +1 -1
  90. package/dist/diff_tool/app/dist/assets/{soy-D4Ndk7-j.js → soy-C2WL9FSb.js} +1 -1
  91. package/dist/diff_tool/app/dist/assets/{sql-DbLQ5SJl.js → sql-CGmNnOfz.js} +1 -1
  92. package/dist/diff_tool/app/dist/assets/{stata-BXoNBosJ.js → stata-CDamV3Uz.js} +1 -1
  93. package/dist/diff_tool/app/dist/assets/{surrealql-toyTYnAL.js → surrealql-BqI4o6tP.js} +1 -1
  94. package/dist/diff_tool/app/dist/assets/{svelte-BEpa7pAW.js → svelte-DPaOUioa.js} +1 -1
  95. package/dist/diff_tool/app/dist/assets/{templ-CZfW_RJI.js → templ-aqrZIEoc.js} +1 -1
  96. package/dist/diff_tool/app/dist/assets/{tex-COemmIIu.js → tex-Bjjtfacp.js} +1 -1
  97. package/dist/diff_tool/app/dist/assets/{ts-tags-CMVUY_BA.js → ts-tags-BGFnsvdQ.js} +1 -1
  98. package/dist/diff_tool/app/dist/assets/{tsx-BTvP6uzd.js → tsx-C_rOxPqZ.js} +1 -1
  99. package/dist/diff_tool/app/dist/assets/{twig-DzP5RjJc.js → twig-DYqVwCOf.js} +1 -1
  100. package/dist/diff_tool/app/dist/assets/{typescript-CQaGlMoX.js → typescript-CppQYc7K.js} +1 -1
  101. package/dist/diff_tool/app/dist/assets/typst-KIbA0FZG.js +1 -0
  102. package/dist/diff_tool/app/dist/assets/{vue-html-BA_OH0Xt.js → vue-html-DcZpMui9.js} +1 -1
  103. package/dist/diff_tool/app/dist/assets/{vue-DaVWTtvb.js → vue-iOYoJEJi.js} +1 -1
  104. package/dist/diff_tool/app/dist/assets/{vue-vine-C_H4XbNR.js → vue-vine-C6v6c0Lr.js} +1 -1
  105. package/dist/diff_tool/app/dist/assets/{xml-1JLzpunp.js → xml-zR0RltJO.js} +1 -1
  106. package/dist/diff_tool/app/dist/assets/{xsl-GtZi0Erj.js → xsl-B9pBLsIV.js} +1 -1
  107. package/dist/diff_tool/app/dist/assets/{yaml-LueTtDDo.js → yaml-sicIihvu.js} +1 -1
  108. package/dist/diff_tool/app/dist/index.html +1 -1
  109. package/dist/tui/listen_capture.js +84 -20
  110. package/dist/tui/listen_capture.js.map +1 -1
  111. package/dist/tui/session_chat_controller.js +213 -29
  112. package/dist/tui/session_chat_controller.js.map +1 -1
  113. package/dist/tui/speech_playback.js +3 -1
  114. package/dist/tui/speech_playback.js.map +1 -1
  115. package/dist/tui/ui/footer.js +6 -6
  116. package/dist/tui/ui/footer.js.map +1 -1
  117. package/package.json +9 -9
  118. package/dist/diff_tool/app/dist/assets/typst-DI99ib-x.js +0 -1
@@ -24,9 +24,10 @@ import { InterruptLifecycle } from "./chat_controller/interrupt_lifecycle.js";
24
24
  import { formatDurationMs, formatFooterTokenCount, formatSessionCost, } from "./chat_controller/status_format.js";
25
25
  import { copyTextToClipboard } from "./clipboard.js";
26
26
  import { DOUBLE_PRESS_WINDOW_MS } from "./constants.js";
27
- import { cleanupListenTempFile, createListenTempFilePath, getSpeechToTextApiKey, getSpeechToTextApiKeyErrorMessage, LISTEN_RECORDING_MAX_DURATION_MS, LISTEN_RECORDING_MIN_BYTES, readListenAudio, startListenAudioCapture, transcribeListenAudio, } from "./listen_capture.js";
27
+ import { cleanupListenTempFile, createListenTempFilePath, createListenTranscription, deleteListenTempFile, getSpeechToTextApiKey, getSpeechToTextApiKeyErrorMessage, LISTEN_CAPTURE_START_TIMEOUT_MS, LISTEN_RECORDING_MAX_DURATION_MS, LISTEN_RECORDING_MIN_BYTES, readListenAudio, startListenAudioCapture, } from "./listen_capture.js";
28
28
  import { createSdkDiffSnapshotDeps, createSdkToolExecutionBackend, } from "./session_tool_execution_backend.js";
29
29
  import { runSpeechPlaybackTask } from "./speech_playback.js";
30
+ const LISTEN_CAPTURE_START_CANCELLED = Symbol("listen capture start cancelled");
30
31
  export class SessionChatController {
31
32
  view;
32
33
  session;
@@ -37,6 +38,7 @@ export class SessionChatController {
37
38
  defaultDiffTool;
38
39
  diffToolLauncher;
39
40
  deps;
41
+ speechToTextDeps;
40
42
  themeIds;
41
43
  commandRegistry;
42
44
  commandHandlers;
@@ -73,8 +75,11 @@ export class SessionChatController {
73
75
  turnTimer;
74
76
  lastEmptySubmitAt;
75
77
  listenRecording;
78
+ retainedListenAudioPath;
76
79
  listenTransition;
77
- isTranscribingListen = false;
80
+ listenStartupAbortController;
81
+ activeListenTranscription;
82
+ listenActivityLabel;
78
83
  speechActivityLabel;
79
84
  speakTask;
80
85
  constructor(options) {
@@ -89,6 +94,7 @@ export class SessionChatController {
89
94
  this.defaultDiffTool = options.defaultDiffTool;
90
95
  this.diffToolLauncher = options.diffToolLauncher;
91
96
  this.deps = options.deps ?? createDefaultCoreDeps();
97
+ this.speechToTextDeps = options.speechToTextDeps;
92
98
  this.themeIds = options.themeIds ?? [];
93
99
  this.observedSessionRevision = options.snapshot.revision;
94
100
  this.commandRegistry = createCommandRegistry();
@@ -104,7 +110,7 @@ export class SessionChatController {
104
110
  compactSummaryOnly: (extra) => this.compactSession("summary-only", extra),
105
111
  compactSummaryAndLast: (extra) => this.compactSession("summary-and-last", extra),
106
112
  reload: () => this.reloadContent(),
107
- listen: () => this.startListenCaptureFromCommand(),
113
+ listen: (action) => this.handleListenCommand(action),
108
114
  speak: () => this.speakLastAssistantMessage(),
109
115
  persona: (id) => this.setPersona(id),
110
116
  prompt: (id) => this.insertPrompt(id),
@@ -142,6 +148,8 @@ export class SessionChatController {
142
148
  this.ephemeralUnsubscribe?.();
143
149
  this.pendingUserMessagesUnsubscribe?.();
144
150
  this.subagentActivitiesUnsubscribe?.();
151
+ this.listenStartupAbortController?.abort(LISTEN_CAPTURE_START_CANCELLED);
152
+ this.activeListenTranscription?.abort();
145
153
  if (this.listenTransition) {
146
154
  await this.listenTransition;
147
155
  }
@@ -524,6 +532,11 @@ export class SessionChatController {
524
532
  this.view.showFooterNotice("interrupted", "default");
525
533
  return;
526
534
  }
535
+ if (this.listenStartupAbortController) {
536
+ this.listenStartupAbortController.abort(LISTEN_CAPTURE_START_CANCELLED);
537
+ this.view.showFooterNotice("interrupted", "default");
538
+ return;
539
+ }
527
540
  if (this.listenRecording) {
528
541
  void this.runListenTransition(() => this.stopListenCapture());
529
542
  return;
@@ -552,9 +565,28 @@ export class SessionChatController {
552
565
  ]);
553
566
  }
554
567
  }
568
+ async handleListenCommand(action) {
569
+ switch (action) {
570
+ case "record":
571
+ await this.startListenCaptureFromCommand();
572
+ return;
573
+ case "retry":
574
+ await this.retryRetainedListenAudio();
575
+ return;
576
+ case "discard":
577
+ await this.discardRetainedListenAudio();
578
+ return;
579
+ }
580
+ }
555
581
  async toggleListenCapture() {
556
582
  if (this.listenTransition) {
557
- this.view.showFooterNotice("speech recording state change already in progress", "default");
583
+ if (this.listenStartupAbortController) {
584
+ this.listenStartupAbortController.abort(LISTEN_CAPTURE_START_CANCELLED);
585
+ this.view.showFooterNotice("cancelled speech recording startup", "default");
586
+ }
587
+ else {
588
+ this.view.showFooterNotice("speech recording state change already in progress", "default");
589
+ }
558
590
  return;
559
591
  }
560
592
  if (this.listenRecording) {
@@ -572,7 +604,7 @@ export class SessionChatController {
572
604
  this.view.showFooterNotice("speech recording already in progress", "default");
573
605
  return;
574
606
  }
575
- if (this.isTranscribingListen) {
607
+ if (this.listenActivityLabel) {
576
608
  this.view.showFooterNotice("speech transcription already in progress", "default");
577
609
  return;
578
610
  }
@@ -605,20 +637,55 @@ export class SessionChatController {
605
637
  ]);
606
638
  return;
607
639
  }
640
+ const retainedAudioPath = this.retainedListenAudioPath;
608
641
  let audioPath;
642
+ let transcription;
643
+ let abortController;
644
+ let completion;
609
645
  try {
610
646
  audioPath = await createListenTempFilePath(this.deps);
611
- const abortController = new AbortController();
612
- const completion = startListenAudioCapture({
647
+ transcription = this.createSpeechTranscription("streaming");
648
+ abortController = new AbortController();
649
+ this.listenStartupAbortController = abortController;
650
+ const capture = startListenAudioCapture({
613
651
  deps: this.deps,
614
652
  audioPath,
615
653
  signal: abortController.signal,
654
+ onAudioChunk: (audio) => transcription?.appendAudio(audio),
616
655
  });
656
+ completion = capture.completion;
657
+ await this.waitForListenCaptureStart(capture.started, abortController);
658
+ if (abortController.signal.aborted) {
659
+ throw abortController.signal.reason;
660
+ }
661
+ if (this.listenStartupAbortController === abortController) {
662
+ this.listenStartupAbortController = undefined;
663
+ }
664
+ if (retainedAudioPath) {
665
+ try {
666
+ await deleteListenTempFile(retainedAudioPath);
667
+ if (this.retainedListenAudioPath === retainedAudioPath) {
668
+ this.retainedListenAudioPath = undefined;
669
+ }
670
+ }
671
+ catch (error) {
672
+ abortController.abort();
673
+ transcription.abort();
674
+ await completion.catch(() => undefined);
675
+ await cleanupListenTempFile(audioPath);
676
+ this.view.addTranscriptNotice("failed to replace retained recording", "error", [
677
+ error.message,
678
+ `recording retained at ${retainedAudioPath}`,
679
+ ]);
680
+ return;
681
+ }
682
+ }
617
683
  const recording = {
618
684
  audioPath,
619
685
  stopRequested: false,
620
686
  abortController,
621
687
  completion,
688
+ transcription,
622
689
  };
623
690
  recording.maxDurationTimeout = setTimeout(() => {
624
691
  if (this.listenRecording !== recording || this.listenTransition)
@@ -631,10 +698,43 @@ export class SessionChatController {
631
698
  void this.watchListenRecording(recording);
632
699
  }
633
700
  catch (err) {
701
+ const cancelled = abortController?.signal.reason === LISTEN_CAPTURE_START_CANCELLED;
702
+ abortController?.abort();
703
+ transcription?.abort();
704
+ await completion?.catch(() => undefined);
634
705
  if (audioPath) {
635
706
  await cleanupListenTempFile(audioPath);
636
707
  }
637
- this.view.addTranscriptNotice("failed to start recording", "error", [err.message]);
708
+ if (!cancelled) {
709
+ this.view.addTranscriptNotice("failed to start recording", "error", [
710
+ err.message,
711
+ ]);
712
+ }
713
+ }
714
+ finally {
715
+ if (this.listenStartupAbortController === abortController) {
716
+ this.listenStartupAbortController = undefined;
717
+ }
718
+ }
719
+ }
720
+ async waitForListenCaptureStart(started, abortController) {
721
+ let timeout;
722
+ try {
723
+ await Promise.race([
724
+ started,
725
+ new Promise((_resolve, reject) => {
726
+ timeout = setTimeout(() => {
727
+ const error = new Error("timed out waiting for microphone audio");
728
+ abortController.abort(error);
729
+ reject(error);
730
+ }, LISTEN_CAPTURE_START_TIMEOUT_MS);
731
+ timeout.unref?.();
732
+ }),
733
+ ]);
734
+ }
735
+ finally {
736
+ if (timeout)
737
+ clearTimeout(timeout);
638
738
  }
639
739
  }
640
740
  async stopListenCapture() {
@@ -651,39 +751,120 @@ export class SessionChatController {
651
751
  await recording.completion;
652
752
  }
653
753
  catch (err) {
754
+ recording.transcription.abort();
654
755
  this.view.addTranscriptNotice("failed to record audio", "error", [err.message]);
655
756
  await cleanupListenTempFile(recording.audioPath);
656
757
  return;
657
758
  }
658
- this.isTranscribingListen = true;
659
- this.refreshStatus();
759
+ await this.transcribeListenAudioFile(recording.audioPath, recording.transcription);
760
+ }
761
+ async retryRetainedListenAudio() {
762
+ if (this.listenRecording || this.listenTransition || this.listenActivityLabel) {
763
+ this.view.showFooterNotice("speech recording state change already in progress", "default");
764
+ return;
765
+ }
766
+ const audioPath = this.retainedListenAudioPath;
767
+ if (!audioPath) {
768
+ this.view.showFooterNotice("no failed speech recording to retry", "default");
769
+ return;
770
+ }
771
+ await this.runListenTransition(() => this.transcribeListenAudioFile(audioPath));
772
+ }
773
+ async discardRetainedListenAudio() {
774
+ if (this.listenRecording || this.listenTransition || this.listenActivityLabel) {
775
+ this.view.showFooterNotice("speech recording state change already in progress", "default");
776
+ return;
777
+ }
778
+ const audioPath = this.retainedListenAudioPath;
779
+ if (!audioPath) {
780
+ this.view.showFooterNotice("no failed speech recording to discard", "default");
781
+ return;
782
+ }
660
783
  try {
661
- const audio = await readListenAudio(recording.audioPath);
662
- if (audio.byteLength < LISTEN_RECORDING_MIN_BYTES) {
663
- this.view.showFooterNotice("recording too short, try again", "default");
664
- return;
784
+ await deleteListenTempFile(audioPath);
785
+ this.retainedListenAudioPath = undefined;
786
+ this.view.showFooterNotice("discarded retained speech recording", "default");
787
+ }
788
+ catch (error) {
789
+ this.view.addTranscriptNotice("failed to discard speech recording", "error", [
790
+ error.message,
791
+ `recording retained at ${audioPath}`,
792
+ ]);
793
+ }
794
+ }
795
+ createSpeechTranscription(mode) {
796
+ return createListenTranscription({
797
+ config: this.config,
798
+ deps: this.deps,
799
+ mode,
800
+ context: collectSpeechToTextContext(this.snapshot),
801
+ speechToTextDeps: this.speechToTextDeps,
802
+ });
803
+ }
804
+ async transcribeListenAudioFile(audioPath, transcription) {
805
+ let audio;
806
+ try {
807
+ audio = await readListenAudio(audioPath);
808
+ }
809
+ catch (error) {
810
+ transcription?.abort();
811
+ this.view.addTranscriptNotice("failed to read speech recording", "error", [
812
+ error.message,
813
+ ]);
814
+ if (this.retainedListenAudioPath === audioPath) {
815
+ this.retainedListenAudioPath = undefined;
665
816
  }
666
- const transcript = await transcribeListenAudio({
667
- config: this.config,
668
- deps: this.deps,
817
+ await cleanupListenTempFile(audioPath);
818
+ return;
819
+ }
820
+ if (audio.byteLength < LISTEN_RECORDING_MIN_BYTES) {
821
+ transcription?.abort();
822
+ this.view.showFooterNotice("recording too short, try again", "default");
823
+ if (this.retainedListenAudioPath === audioPath) {
824
+ this.retainedListenAudioPath = undefined;
825
+ }
826
+ await cleanupListenTempFile(audioPath);
827
+ return;
828
+ }
829
+ let activeTranscription = transcription;
830
+ this.listenActivityLabel = "transcribing voice input";
831
+ this.refreshStatus();
832
+ try {
833
+ activeTranscription ??= this.createSpeechTranscription("file");
834
+ this.activeListenTranscription = activeTranscription;
835
+ const text = await activeTranscription.finish({
669
836
  audio,
670
- context: collectSpeechToTextContext(this.snapshot),
837
+ mimeType: "audio/wav",
838
+ fileName: "speech.wav",
839
+ language: "en",
671
840
  });
672
- const text = transcript.trim();
673
- if (!text) {
674
- return;
675
- }
676
841
  this.view.insertEditorTextAtCursor(text);
842
+ this.retainedListenAudioPath = undefined;
843
+ try {
844
+ await deleteListenTempFile(audioPath);
845
+ }
846
+ catch (error) {
847
+ this.view.addTranscriptNotice("failed to delete speech recording", "error", [
848
+ error.message,
849
+ `recording remains at ${audioPath}; delete it manually`,
850
+ ]);
851
+ }
677
852
  }
678
- catch (err) {
853
+ catch (error) {
854
+ this.retainedListenAudioPath = audioPath;
679
855
  this.view.addTranscriptNotice("failed to transcribe speech", "error", [
680
- err.message,
856
+ error.message,
857
+ `recording retained at ${audioPath}`,
858
+ "run /listen retry to try again, or /listen discard to delete it",
681
859
  ]);
682
860
  }
683
861
  finally {
684
- this.isTranscribingListen = false;
862
+ activeTranscription?.abort();
863
+ if (this.activeListenTranscription === activeTranscription) {
864
+ this.activeListenTranscription = undefined;
865
+ }
866
+ this.listenActivityLabel = undefined;
685
867
  this.refreshStatus();
686
- await cleanupListenTempFile(recording.audioPath);
687
868
  }
688
869
  }
689
870
  async cancelListenCapture() {
@@ -696,6 +877,7 @@ export class SessionChatController {
696
877
  this.view.setEditorInputEnabled(true);
697
878
  this.refreshStatus();
698
879
  recording.abortController.abort();
880
+ recording.transcription.abort();
699
881
  try {
700
882
  await recording.completion;
701
883
  }
@@ -711,6 +893,7 @@ export class SessionChatController {
711
893
  if (this.listenRecording !== recording || recording.stopRequested)
712
894
  return;
713
895
  this.listenRecording = undefined;
896
+ recording.transcription.abort();
714
897
  this.view.setEditorInputEnabled(true);
715
898
  this.refreshStatus();
716
899
  const detail = result.exitCode !== null
@@ -726,6 +909,7 @@ export class SessionChatController {
726
909
  if (this.listenRecording !== recording || recording.stopRequested)
727
910
  return;
728
911
  this.listenRecording = undefined;
912
+ recording.transcription.abort();
729
913
  this.view.setEditorInputEnabled(true);
730
914
  this.refreshStatus();
731
915
  const error = err;
@@ -1603,8 +1787,8 @@ export class SessionChatController {
1603
1787
  if (this.manualCompactionInProgress || this.hasRunningCompactionOperation(this.snapshot)) {
1604
1788
  return "compacting context";
1605
1789
  }
1606
- if (this.isTranscribingListen) {
1607
- return "transcribing voice input";
1790
+ if (this.listenActivityLabel) {
1791
+ return this.listenActivityLabel;
1608
1792
  }
1609
1793
  return this.diffReviewService.getActivityLabel(this.speechActivityLabel);
1610
1794
  }
@@ -1772,7 +1956,7 @@ export class SessionChatController {
1772
1956
  return (!this.isSessionOperationActive() &&
1773
1957
  !this.listenRecording &&
1774
1958
  !this.listenTransition &&
1775
- !this.isTranscribingListen &&
1959
+ !this.listenActivityLabel &&
1776
1960
  !this.speakTask);
1777
1961
  }
1778
1962
  resolveDiffToolConfig() {