diff --git a/app/src-tauri/src/models/project.rs b/app/src-tauri/src/models/project.rs index ab7c57f..fc0937f 100644 --- a/app/src-tauri/src/models/project.rs +++ b/app/src-tauri/src/models/project.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -93,6 +95,9 @@ pub struct Project { pub enabled_mcp_servers: Vec, #[serde(default)] pub claude_code_settings: Option, + /// User-defined display names for terminal tabs, keyed by session id. + #[serde(default)] + pub renamed_session_names: HashMap, pub created_at: String, pub updated_at: String, } @@ -217,6 +222,7 @@ impl Project { claude_instructions: None, enabled_mcp_servers: Vec::new(), claude_code_settings: None, + renamed_session_names: HashMap::new(), created_at: now.clone(), updated_at: now, } diff --git a/app/src/components/terminal/TerminalTabs.tsx b/app/src/components/terminal/TerminalTabs.tsx index 14ac0c3..91dcc98 100644 --- a/app/src/components/terminal/TerminalTabs.tsx +++ b/app/src/components/terminal/TerminalTabs.tsx @@ -1,7 +1,38 @@ +import { useEffect, useRef, useState } from "react"; import { useTerminal } from "../../hooks/useTerminal"; +import { useProjects } from "../../hooks/useProjects"; + +interface ContextMenuState { + sessionId: string; + x: number; + y: number; +} export default function TerminalTabs() { const { sessions, activeSessionId, setActiveSession, close } = useTerminal(); + const { projects, update } = useProjects(); + const [menu, setMenu] = useState(null); + const [renamingId, setRenamingId] = useState(null); + const [renameDraft, setRenameDraft] = useState(""); + const renameInputRef = useRef(null); + + useEffect(() => { + if (!menu) return; + const dismiss = () => setMenu(null); + window.addEventListener("click", dismiss); + window.addEventListener("scroll", dismiss, true); + return () => { + window.removeEventListener("click", dismiss); + window.removeEventListener("scroll", dismiss, true); + }; + }, [menu]); + + useEffect(() => { + if (renamingId) { + renameInputRef.current?.focus(); + renameInputRef.current?.select(); + } + }, [renamingId]); if (sessions.length === 0) { return ( @@ -11,33 +42,160 @@ export default function TerminalTabs() { ); } + const getCustomName = (projectId: string, sessionId: string): string | null => { + const project = projects.find((p) => p.id === projectId); + return project?.renamed_session_names?.[sessionId] ?? null; + }; + + const startRename = (sessionId: string) => { + const session = sessions.find((s) => s.id === sessionId); + if (!session) return; + const current = getCustomName(session.projectId, sessionId) ?? session.sessionName ?? session.projectName; + setRenameDraft(current); + setRenamingId(sessionId); + setMenu(null); + }; + + const commitRename = async (sessionId: string) => { + const session = sessions.find((s) => s.id === sessionId); + if (!session) { + setRenamingId(null); + return; + } + const project = projects.find((p) => p.id === session.projectId); + if (!project) { + setRenamingId(null); + return; + } + const trimmed = renameDraft.trim(); + const map = { ...(project.renamed_session_names ?? {}) }; + if (trimmed) { + map[sessionId] = trimmed; + } else { + delete map[sessionId]; + } + try { + await update({ ...project, renamed_session_names: map }); + } catch (err) { + console.error("Failed to rename terminal tab:", err); + } finally { + setRenamingId(null); + } + }; + + const clearCustomName = async (sessionId: string) => { + const session = sessions.find((s) => s.id === sessionId); + if (!session) return; + const project = projects.find((p) => p.id === session.projectId); + if (!project) return; + const map = { ...(project.renamed_session_names ?? {}) }; + if (!(sessionId in map)) { + setMenu(null); + return; + } + delete map[sessionId]; + try { + await update({ ...project, renamed_session_names: map }); + } catch (err) { + console.error("Failed to reset terminal tab name:", err); + } finally { + setMenu(null); + } + }; + return (
- {sessions.map((session) => ( -
setActiveSession(session.id)} - className={`flex items-center gap-2 px-3 h-full text-xs cursor-pointer border-r border-[var(--border-color)] transition-colors ${ - activeSessionId === session.id - ? "bg-[var(--bg-primary)] text-[var(--text-primary)]" - : "text-[var(--text-secondary)] hover:text-[var(--text-primary)]" - }`} - > - - {session.sessionName ?? session.projectName}{session.sessionType === "bash" ? " (bash)" : ""} - - -
- ))} + {isRenaming ? ( + setRenameDraft(e.target.value)} + onClick={(e) => e.stopPropagation()} + onBlur={() => commitRename(session.id)} + onKeyDown={(e) => { + if (e.key === "Enter") (e.target as HTMLInputElement).blur(); + if (e.key === "Escape") setRenamingId(null); + }} + className="max-w-[180px] px-1 py-0 bg-[var(--bg-primary)] border border-[var(--accent)] rounded text-xs text-[var(--text-primary)] focus:outline-none" + /> + ) : ( + + {displayLabel} + + )} + +
+ ); + })} + {menu && (() => { + const session = sessions.find((s) => s.id === menu.sessionId); + const hasCustom = session ? !!getCustomName(session.projectId, menu.sessionId) : false; + return ( +
e.stopPropagation()} + > + + {hasCustom && ( + + )} +
+ +
+ ); + })()}
); } diff --git a/app/src/hooks/useTerminal.ts b/app/src/hooks/useTerminal.ts index 2521f3d..0539263 100644 --- a/app/src/hooks/useTerminal.ts +++ b/app/src/hooks/useTerminal.ts @@ -28,8 +28,25 @@ export function useTerminal() { const close = useCallback( async (sessionId: string) => { + // Capture session/project info before we drop it from local state. + const { sessions: currentSessions, projects } = useAppState.getState(); + const session = currentSessions.find((s) => s.id === sessionId); + const project = session ? projects.find((p) => p.id === session.projectId) : undefined; + await commands.closeTerminalSession(sessionId); removeSession(sessionId); + + // Drop any persisted custom name for this session. + if (project && project.renamed_session_names && sessionId in project.renamed_session_names) { + const map = { ...project.renamed_session_names }; + delete map[sessionId]; + try { + const updated = await commands.updateProject({ ...project, renamed_session_names: map }); + useAppState.getState().updateProjectInList(updated); + } catch (err) { + console.error("Failed to clear renamed tab name on close:", err); + } + } }, [removeSession], ); diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index fd546b6..b6e4d79 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -37,6 +37,7 @@ export interface Project { claude_instructions: string | null; enabled_mcp_servers: string[]; claude_code_settings: ClaudeCodeSettings | null; + renamed_session_names: Record; created_at: string; updated_at: string; }