Move mic button from terminal overlay to project action buttons
All checks were successful
Build App / build-macos (push) Successful in 2m53s
Build App / build-windows (push) Successful in 3m26s
Build App / build-linux (push) Successful in 5m59s
Build App / sync-to-github (push) Successful in 11s

Relocates the voice/mic toggle from a floating overlay on the terminal
view to the project command row (alongside Stop, Terminal, Config) so
it no longer blocks access to the terminal window.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 06:58:03 -08:00
parent c5e28f9caa
commit 33f02e65c0
2 changed files with 46 additions and 32 deletions

View File

@@ -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<string | null>(null);
const [showConfig, setShowConfig] = useState(false);
@@ -371,6 +382,9 @@ export default function ProjectCard({ project }: Props) {
<>
<ActionButton onClick={handleStop} disabled={loading} label="Stop" />
<ActionButton onClick={handleOpenTerminal} disabled={loading} label="Terminal" accent />
{projectSession && (
<MicButton voice={voice} />
)}
</>
) : (
<>
@@ -869,3 +883,35 @@ function ActionButton({
</button>
);
}
function MicButton({ voice }: { voice: ReturnType<typeof useVoice> }) {
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 (
<button
onClick={(e) => { e.stopPropagation(); voice.toggle(); }}
disabled={voice.state === "starting"}
title={
voice.state === "active"
? "Voice active — click to stop"
: voice.error
? `Voice error: ${voice.error}`
: "Enable voice input for /voice mode"
}
className={`text-xs px-2 py-0.5 rounded transition-colors disabled:opacity-50 ${color} hover:bg-[var(--bg-primary)]`}
>
{voice.state === "active"
? "Mic On"
: voice.state === "starting"
? "Mic..."
: "Mic Off"}
</button>
);
}

View File

@@ -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<WebglAddon | null>(null);
const detectorRef = useRef<UrlDetector | null>(null);
const { sendInput, pasteImage, resize, onOutput, onExit } = useTerminal();
const { appSettings } = useSettings();
const voice = useVoice(sessionId, appSettings?.default_microphone);
const [detectedUrl, setDetectedUrl] = useState<string | null>(null);
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(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}
</div>
)}
<button
onClick={voice.toggle}
title={
voice.state === "active"
? "Voice active — click to stop"
: voice.error
? `Voice error: ${voice.error}`
: "Enable voice input for /voice mode"
}
className={`absolute bottom-4 left-4 z-50 px-3 py-1.5 rounded-md text-xs font-medium border shadow-lg transition-colors cursor-pointer ${
voice.state === "active"
? "bg-[#1a3a2a] text-[#3fb950] border-[#238636] hover:bg-[#243b2a]"
: voice.state === "starting"
? "bg-[#1f2937] text-[#d29922] border-[#30363d] opacity-75"
: voice.state === "error"
? "bg-[#3a1a1a] text-[#ff7b72] border-[#da3633] hover:bg-[#4a2020]"
: "bg-[#1f2937] text-[#b1bac4] border-[#30363d] hover:bg-[#2d3748] hover:text-[#e6edf3]"
}`}
disabled={voice.state === "starting"}
>
{voice.state === "active"
? "Mic On"
: voice.state === "starting"
? "Mic..."
: "Mic Off"}
</button>
{!isAtBottom && (
<button
onClick={handleScrollToBottom}