Compare commits

..

3 Commits

Author SHA1 Message Date
c853f2676d Add tooltip hint for double-click to rename project name
All checks were successful
Build App / build-macos (push) Successful in 3m29s
Build App / build-windows (push) Successful in 3m55s
Build App / build-linux (push) Successful in 4m43s
Build App / sync-to-github (push) Successful in 9s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:27:24 -08:00
090aad6bc6 Fix project rename, remove confirmation, and auth mode change bugs
All checks were successful
Build App / build-macos (push) Successful in 2m43s
Build App / build-windows (push) Successful in 4m31s
Build App / build-linux (push) Successful in 4m41s
Build App / sync-to-github (push) Successful in 9s
- Add double-click-to-rename on project names in the sidebar
- Replace window.confirm() with inline React confirmation for project
  removal (confirm dialog didn't block in Tauri webview)
- Add serde(default) to skip_serializing fields in Rust models so
  deserialization doesn't fail when frontend omits secret fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:39:34 -08:00
c023d80c86 Hide mic toggle UI until upstream /voice WSL detection is fixed
All checks were successful
Build App / build-macos (push) Successful in 2m22s
Build App / build-windows (push) Successful in 3m57s
Build App / build-linux (push) Successful in 5m17s
Build App / sync-to-github (push) Successful in 11s
Claude Code's /voice command incorrectly detects containers running
on WSL2 hosts as unsupported WSL environments. Remove the mic button
from project cards and microphone settings from the settings panel,
but keep useVoice hook and MicrophoneSettings component for re-enabling
once the upstream issue is resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 07:12:35 -08:00
3 changed files with 72 additions and 64 deletions

View File

@@ -35,7 +35,7 @@ pub struct Project {
pub bedrock_config: Option<BedrockConfig>,
pub allow_docker_access: bool,
pub ssh_key_path: Option<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, default)]
pub git_token: Option<String>,
pub git_user_name: Option<String>,
pub git_user_email: Option<String>,
@@ -100,14 +100,14 @@ impl Default for BedrockAuthMethod {
pub struct BedrockConfig {
pub auth_method: BedrockAuthMethod,
pub aws_region: String,
#[serde(skip_serializing)]
#[serde(skip_serializing, default)]
pub aws_access_key_id: Option<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, default)]
pub aws_secret_access_key: Option<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, default)]
pub aws_session_token: Option<String>,
pub aws_profile: Option<String>,
#[serde(skip_serializing)]
#[serde(skip_serializing, default)]
pub aws_bearer_token: Option<String>,
pub model_id: Option<String>,
pub disable_prompt_caching: bool,

View File

@@ -5,8 +5,6 @@ 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";
@@ -23,15 +21,6 @@ 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);
@@ -41,6 +30,9 @@ export default function ProjectCard({ project }: Props) {
const [progressMsg, setProgressMsg] = useState<string | null>(null);
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
const [operationCompleted, setOperationCompleted] = useState(false);
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false);
const [isEditingName, setIsEditingName] = useState(false);
const [editName, setEditName] = useState(project.name);
const isSelected = selectedProjectId === project.id;
const isStopped = project.status === "stopped" || project.status === "error";
@@ -65,6 +57,7 @@ export default function ProjectCard({ project }: Props) {
// Sync local state when project prop changes (e.g., after save or external update)
useEffect(() => {
setEditName(project.name);
setPaths(project.paths ?? []);
setSshKeyPath(project.ssh_key_path ?? "");
setGitName(project.git_user_name ?? "");
@@ -320,7 +313,41 @@ export default function ProjectCard({ project }: Props) {
>
<div className="flex items-center gap-2">
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColor}`} />
<span className="text-sm font-medium truncate flex-1">{project.name}</span>
{isEditingName ? (
<input
autoFocus
value={editName}
onChange={(e) => setEditName(e.target.value)}
onBlur={async () => {
setIsEditingName(false);
const trimmed = editName.trim();
if (trimmed && trimmed !== project.name) {
try {
await update({ ...project, name: trimmed });
} catch (err) {
console.error("Failed to rename project:", err);
setEditName(project.name);
}
} else {
setEditName(project.name);
}
}}
onKeyDown={(e) => {
if (e.key === "Enter") (e.target as HTMLInputElement).blur();
if (e.key === "Escape") { setEditName(project.name); setIsEditingName(false); }
}}
onClick={(e) => e.stopPropagation()}
className="text-sm font-medium flex-1 min-w-0 px-1 py-0 bg-[var(--bg-primary)] border border-[var(--accent)] rounded text-[var(--text-primary)] focus:outline-none"
/>
) : (
<span
className="text-sm font-medium truncate flex-1 cursor-text"
title="Double-click to rename"
onDoubleClick={(e) => { e.stopPropagation(); setIsEditingName(true); }}
>
{project.name}
</span>
)}
</div>
<div className="mt-0.5 ml-4 space-y-0.5">
{project.paths.map((pp, i) => (
@@ -382,9 +409,6 @@ 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} />
)}
</>
) : (
<>
@@ -399,16 +423,34 @@ export default function ProjectCard({ project }: Props) {
disabled={false}
label={showConfig ? "Hide" : "Config"}
/>
<ActionButton
onClick={async () => {
if (confirm(`Remove project "${project.name}"?`)) {
await remove(project.id);
}
}}
disabled={loading}
label="Remove"
danger
/>
{showRemoveConfirm ? (
<span className="inline-flex items-center gap-1 text-xs">
<span className="text-[var(--text-secondary)]">Remove?</span>
<button
onClick={async (e) => {
e.stopPropagation();
setShowRemoveConfirm(false);
await remove(project.id);
}}
className="px-1.5 py-0.5 rounded text-white bg-[var(--error)] hover:opacity-80 transition-colors"
>
Yes
</button>
<button
onClick={(e) => { e.stopPropagation(); setShowRemoveConfirm(false); }}
className="px-1.5 py-0.5 rounded text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
>
No
</button>
</span>
) : (
<ActionButton
onClick={() => setShowRemoveConfirm(true)}
disabled={loading}
label="Remove"
danger
/>
)}
</div>
{/* Config panel */}
@@ -884,34 +926,3 @@ function ActionButton({
);
}
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

@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import ApiKeyInput from "./ApiKeyInput";
import DockerSettings from "./DockerSettings";
import AwsSettings from "./AwsSettings";
import MicrophoneSettings from "./MicrophoneSettings";
import { useSettings } from "../../hooks/useSettings";
import { useUpdates } from "../../hooks/useUpdates";
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
@@ -60,8 +59,6 @@ export default function SettingsPanel() {
<DockerSettings />
<AwsSettings />
<MicrophoneSettings />
{/* Container Timezone */}
<div>
<label className="block text-sm font-medium mb-1">Container Timezone</label>