diff --git a/app/src/components/projects/ProjectCard.tsx b/app/src/components/projects/ProjectCard.tsx index cb108ce..96199f0 100644 --- a/app/src/components/projects/ProjectCard.tsx +++ b/app/src/components/projects/ProjectCard.tsx @@ -5,6 +5,8 @@ import type { Project, ProjectPath, AuthMode, BedrockConfig, BedrockAuthMethod } import { useProjects } from "../../hooks/useProjects"; import { useMcpServers } from "../../hooks/useMcpServers"; import { useTerminal } from "../../hooks/useTerminal"; +import { useSettings } from "../../hooks/useSettings"; +import { useVoice } from "../../hooks/useVoice"; import { useAppState } from "../../store/appState"; import EnvVarsModal from "./EnvVarsModal"; import PortMappingsModal from "./PortMappingsModal"; @@ -21,6 +23,15 @@ export default function ProjectCard({ project }: Props) { const { start, stop, rebuild, remove, update } = useProjects(); const { mcpServers } = useMcpServers(); const { open: openTerminal } = useTerminal(); + const { appSettings } = useSettings(); + const sessions = useAppState(s => s.sessions); + const activeSessionId = useAppState(s => s.activeSessionId); + + // Find the active terminal session for this project (prefer the currently viewed one) + const projectSession = sessions.find(s => s.projectId === project.id && s.id === activeSessionId) + ?? sessions.find(s => s.projectId === project.id); + const voice = useVoice(projectSession?.id ?? "", appSettings?.default_microphone); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [showConfig, setShowConfig] = useState(false); @@ -371,6 +382,9 @@ export default function ProjectCard({ project }: Props) { <> + {projectSession && ( + + )} ) : ( <> @@ -869,3 +883,35 @@ function ActionButton({ ); } + +function MicButton({ voice }: { voice: ReturnType }) { + const color = + voice.state === "active" + ? "text-[var(--success)] hover:text-[var(--success)]" + : voice.state === "starting" + ? "text-[var(--warning)] opacity-75" + : voice.state === "error" + ? "text-[var(--error)] hover:text-[var(--error)]" + : "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"; + + return ( + + ); +} diff --git a/app/src/components/terminal/TerminalView.tsx b/app/src/components/terminal/TerminalView.tsx index a50b50d..2c1e29d 100644 --- a/app/src/components/terminal/TerminalView.tsx +++ b/app/src/components/terminal/TerminalView.tsx @@ -6,8 +6,6 @@ import { WebLinksAddon } from "@xterm/addon-web-links"; import { openUrl } from "@tauri-apps/plugin-opener"; import "@xterm/xterm/css/xterm.css"; import { useTerminal } from "../../hooks/useTerminal"; -import { useSettings } from "../../hooks/useSettings"; -import { useVoice } from "../../hooks/useVoice"; import { UrlDetector } from "../../lib/urlDetector"; import UrlToast from "./UrlToast"; @@ -24,9 +22,6 @@ export default function TerminalView({ sessionId, active }: Props) { const webglRef = useRef(null); const detectorRef = useRef(null); const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal(); - const { appSettings } = useSettings(); - - const voice = useVoice(sessionId, appSettings?.default_microphone); const [detectedUrl, setDetectedUrl] = useState(null); const [imagePasteMsg, setImagePasteMsg] = useState(null); @@ -205,7 +200,6 @@ export default function TerminalView({ sessionId, active }: Props) { try { webglRef.current?.dispose(); } catch { /* may already be disposed */ } webglRef.current = null; term.dispose(); - voice.stop(); }; }, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps @@ -290,32 +284,6 @@ export default function TerminalView({ sessionId, active }: Props) { {imagePasteMsg} )} - {!isAtBottom && (