diff --git a/app/src/components/terminal/SttButton.tsx b/app/src/components/terminal/SttButton.tsx index 4e83e02..9ab40be 100644 --- a/app/src/components/terminal/SttButton.tsx +++ b/app/src/components/terminal/SttButton.tsx @@ -1,15 +1,17 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useSTT } from "../../hooks/useSTT"; +import type { SttState } from "../../hooks/useSTT"; import * as commands from "../../lib/tauri-commands"; interface Props { - sessionId: string; - sendInput: (sessionId: string, data: string) => Promise; + state: SttState; + error: string | null; + onToggle: () => Promise; + onCancel: () => Promise; } -export default function SttButton({ sessionId, sendInput }: Props) { - const { state, error, toggle, cancelRecording } = useSTT(sessionId, sendInput); +export default function SttButton({ state, error, onToggle, onCancel }: Props) { const [elapsed, setElapsed] = useState(0); + const [hovered, setHovered] = useState(false); const timerRef = useRef | null>(null); // Track recording duration @@ -40,17 +42,17 @@ export default function SttButton({ sessionId, sendInput }: Props) { // Container start failed, toggle will still attempt transcription } } - await toggle(); - }, [state, toggle]); + await onToggle(); + }, [state, onToggle]); const handleContextMenu = useCallback( (e: React.MouseEvent) => { e.preventDefault(); if (state === "recording") { - cancelRecording(); + onCancel(); } }, - [state, cancelRecording], + [state, onCancel], ); const formatTime = (seconds: number) => { @@ -60,38 +62,43 @@ export default function SttButton({ sessionId, sendInput }: Props) { }; return ( -
- + {hovered && state !== "recording" && ( +
+ {state === "transcribing" ? "Transcribing..." : ( + <>Speech to text Ctrl+Shift+M + )} +
)} - +
{state === "recording" && ( {formatTime(elapsed)} diff --git a/app/src/components/terminal/TerminalView.tsx b/app/src/components/terminal/TerminalView.tsx index 7230561..f0dfc71 100644 --- a/app/src/components/terminal/TerminalView.tsx +++ b/app/src/components/terminal/TerminalView.tsx @@ -7,6 +7,7 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import "@xterm/xterm/css/xterm.css"; import { useTerminal } from "../../hooks/useTerminal"; import { useAppState } from "../../store/appState"; +import { useSTT } from "../../hooks/useSTT"; import SttButton from "./SttButton"; import { awsSsoRefresh } from "../../lib/tauri-commands"; import { UrlDetector } from "../../lib/urlDetector"; @@ -27,6 +28,9 @@ export default function TerminalView({ sessionId, active }: Props) { const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal(); const setTerminalHasSelection = useAppState(s => s.setTerminalHasSelection); const sttEnabled = useAppState(s => s.appSettings?.stt?.enabled); + const stt = useSTT(sessionId, sendInput); + const sttToggleRef = useRef(stt.toggle); + sttToggleRef.current = stt.toggle; const ssoBufferRef = useRef(""); const ssoTriggeredRef = useRef(false); @@ -102,6 +106,11 @@ export default function TerminalView({ sessionId, active }: Props) { } return false; // prevent xterm from processing this key } + // Ctrl+Shift+M toggles speech-to-text recording + if (event.type === "keydown" && event.ctrlKey && event.shiftKey && event.key === "M") { + sttToggleRef.current(); + return false; + } return true; }); @@ -427,7 +436,7 @@ export default function TerminalView({ sessionId, active }: Props) { {isAutoFollow ? "▼ Following" : "▽ Paused"} {/* STT mic button - bottom left */} - {sttEnabled && } + {sttEnabled && } {/* Jump to Current - bottom right, when scrolled up */} {!isAtBottom && (