fix(terminal): rename Claude terminals only once on initial message (#1366)

* fix(terminal): rename Claude terminals only once on initial message

- Add autoNameClaudeTerminals setting (defaults to true) to control
  whether Claude terminals should be auto-named based on first message
- Add claudeNamedOnce flag to terminal store to track if terminal
  has already been renamed (prevents repeated renames on each message)
- Update useAutoNaming hook to only trigger rename once in Claude mode
- Add toggle to Developer Tools settings section
- Add i18n translations for English and French

This fixes the issue where Claude terminals were being renamed on every
message sent to Claude instead of just once on the initial message.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(terminal): address code review suggestions

- Re-fetch terminal state after async generateTerminalName to avoid
  stale closure when checking isClaudeMode
- Add comment explaining nullish coalescing fallback for new setting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-20 18:18:44 +01:00
committed by GitHub
parent 317d5e9488
commit b2d2d7e9eb
7 changed files with 112 additions and 36 deletions
@@ -365,6 +365,31 @@ export function DevToolsSettings({ settings, onSettingsChange }: DevToolsSetting
)} )}
</div> </div>
{/* Auto-name Claude Terminals Toggle */}
<div className="space-y-3 pt-2 border-t border-border">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="auto-name-claude-terminals" className="text-sm font-medium">
{t('devtools.autoNameClaude.label', 'Auto-name Claude terminals')}
</Label>
<p className="text-xs text-muted-foreground">
{t('devtools.autoNameClaude.description', 'Use AI to generate a descriptive name for Claude terminals based on your first message')}
</p>
</div>
{/* Fallback to true for existing users who don't have this setting in persisted config */}
<Switch
id="auto-name-claude-terminals"
checked={settings.autoNameClaudeTerminals ?? true}
onCheckedChange={(checked) => {
onSettingsChange({
...settings,
autoNameClaudeTerminals: checked
});
}}
/>
</div>
</div>
{/* YOLO Mode Toggle */} {/* YOLO Mode Toggle */}
<div className="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-4"> <div className="space-y-3 rounded-md border border-amber-500/30 bg-amber-500/5 p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -11,51 +11,72 @@ export function useAutoNaming({ terminalId, cwd }: UseAutoNamingOptions) {
const lastCommandRef = useRef<string>(''); const lastCommandRef = useRef<string>('');
const autoNameTimeoutRef = useRef<NodeJS.Timeout | null>(null); const autoNameTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const autoNameTerminals = useSettingsStore((state) => state.settings.autoNameTerminals); const autoNameTerminals = useSettingsStore((state) => state.settings.autoNameTerminals);
const autoNameClaudeTerminals = useSettingsStore((state) => state.settings.autoNameClaudeTerminals);
const terminal = useTerminalStore((state) => state.terminals.find((t) => t.id === terminalId)); const terminal = useTerminalStore((state) => state.terminals.find((t) => t.id === terminalId));
const updateTerminal = useTerminalStore((state) => state.updateTerminal); const updateTerminal = useTerminalStore((state) => state.updateTerminal);
const setClaudeNamedOnce = useTerminalStore((state) => state.setClaudeNamedOnce);
const triggerAutoNaming = useCallback(async () => { const triggerAutoNaming = useCallback(async () => {
if (!autoNameTerminals || terminal?.isClaudeMode || !lastCommandRef.current.trim()) { // Check if we have a command to base the name on
if (!lastCommandRef.current.trim()) {
return; return;
} }
const command = lastCommandRef.current.trim(); // Handle Claude mode vs regular terminal mode
const commandLower = command.toLowerCase(); if (terminal?.isClaudeMode) {
const firstWord = commandLower.split(/\s+/)[0]; // In Claude mode: only rename if autoNameClaudeTerminals is enabled AND we haven't named yet
if (!autoNameClaudeTerminals || terminal?.claudeNamedOnce) {
return;
}
} else {
// Regular terminal mode: use the standard autoNameTerminals setting
if (!autoNameTerminals) {
return;
}
}
// Skip very short commands const command = lastCommandRef.current.trim();
// Skip very short commands/messages
if (command.length < 3) { if (command.length < 3) {
return; return;
} }
// Skip common shell/navigation commands that don't represent meaningful work. // In Claude mode, messages are natural language prompts, not shell commands
// These commands are too generic to produce useful terminal names - they don't indicate // Skip the shell command filtering since we want to name based on the first prompt
// a specific task or purpose. For example, "git" could be any git operation, if (!terminal?.isClaudeMode) {
// "npm" could be install, run, or test. Meaningful names come from project-specific const commandLower = command.toLowerCase();
// commands like "npm run build:prod" or application-specific scripts. const firstWord = commandLower.split(/\s+/)[0];
const skipCommands = [
// Navigation & file listing
'ls', 'cd', 'll', 'la', 'pwd', 'dir', 'tree',
// Shell control
'exit', 'clear', 'cls', 'reset', 'history',
// Claude CLI - naming should come from the task description inside Claude, not the launch command
'claude',
// Common dev tools that are too generic
'git', 'npm', 'yarn', 'pnpm', 'node', 'python', 'pip', 'cargo', 'go',
'docker', 'kubectl', 'make', 'cmake',
// Package managers
'brew', 'apt', 'yum', 'pacman', 'choco', 'scoop', 'winget',
// Editors
'vim', 'nvim', 'nano', 'code', 'cursor',
// System commands
'cat', 'head', 'tail', 'less', 'more', 'grep', 'find', 'which', 'where',
'echo', 'env', 'export', 'set', 'unset', 'alias', 'source',
'chmod', 'chown', 'mkdir', 'rmdir', 'rm', 'cp', 'mv', 'touch',
'man', 'help', 'whoami', 'hostname', 'date', 'time', 'top', 'htop', 'ps',
];
if (skipCommands.includes(firstWord)) { // Skip common shell/navigation commands that don't represent meaningful work.
return; // These commands are too generic to produce useful terminal names - they don't indicate
// a specific task or purpose. For example, "git" could be any git operation,
// "npm" could be install, run, or test. Meaningful names come from project-specific
// commands like "npm run build:prod" or application-specific scripts.
const skipCommands = [
// Navigation & file listing
'ls', 'cd', 'll', 'la', 'pwd', 'dir', 'tree',
// Shell control
'exit', 'clear', 'cls', 'reset', 'history',
// Claude CLI - naming should come from the task description inside Claude, not the launch command
'claude',
// Common dev tools that are too generic
'git', 'npm', 'yarn', 'pnpm', 'node', 'python', 'pip', 'cargo', 'go',
'docker', 'kubectl', 'make', 'cmake',
// Package managers
'brew', 'apt', 'yum', 'pacman', 'choco', 'scoop', 'winget',
// Editors
'vim', 'nvim', 'nano', 'code', 'cursor',
// System commands
'cat', 'head', 'tail', 'less', 'more', 'grep', 'find', 'which', 'where',
'echo', 'env', 'export', 'set', 'unset', 'alias', 'source',
'chmod', 'chown', 'mkdir', 'rmdir', 'rm', 'cp', 'mv', 'touch',
'man', 'help', 'whoami', 'hostname', 'date', 'time', 'top', 'htop', 'ps',
];
if (skipCommands.includes(firstWord)) {
return;
}
} }
try { try {
@@ -64,11 +85,18 @@ export function useAutoNaming({ terminalId, cwd }: UseAutoNamingOptions) {
updateTerminal(terminalId, { title: result.data }); updateTerminal(terminalId, { title: result.data });
// Sync to main process so title persists across hot reloads // Sync to main process so title persists across hot reloads
window.electronAPI.setTerminalTitle(terminalId, result.data); window.electronAPI.setTerminalTitle(terminalId, result.data);
// Mark Claude terminal as named once to prevent repeated renames
// Re-fetch terminal state after async operation to avoid stale closure
const currentTerminal = useTerminalStore.getState().terminals.find((t) => t.id === terminalId);
if (currentTerminal?.isClaudeMode) {
setClaudeNamedOnce(terminalId, true);
}
} }
} catch (error) { } catch (error) {
console.warn('[Terminal] Auto-naming failed:', error); console.warn('[Terminal] Auto-naming failed:', error);
} }
}, [autoNameTerminals, terminal?.isClaudeMode, terminal?.cwd, cwd, terminalId, updateTerminal]); }, [autoNameTerminals, autoNameClaudeTerminals, terminal?.isClaudeMode, terminal?.claudeNamedOnce, terminal?.cwd, cwd, terminalId, updateTerminal, setClaudeNamedOnce]);
const handleCommandEnter = useCallback((command: string) => { const handleCommandEnter = useCallback((command: string) => {
lastCommandRef.current = command; lastCommandRef.current = command;
@@ -94,6 +94,7 @@ export interface Terminal {
isClaudeBusy?: boolean; // Whether Claude Code is actively processing (for visual indicator) isClaudeBusy?: boolean; // Whether Claude Code is actively processing (for visual indicator)
pendingClaudeResume?: boolean; // Whether this terminal has a pending Claude resume (deferred until tab activated) pendingClaudeResume?: boolean; // Whether this terminal has a pending Claude resume (deferred until tab activated)
displayOrder?: number; // Display order for tab persistence (lower = further left) displayOrder?: number; // Display order for tab persistence (lower = further left)
claudeNamedOnce?: boolean; // Whether this Claude terminal has been auto-named based on initial message (prevents repeated naming)
} }
interface TerminalLayout { interface TerminalLayout {
@@ -126,6 +127,7 @@ interface TerminalState {
setWorktreeConfig: (id: string, config: TerminalWorktreeConfig | undefined) => void; setWorktreeConfig: (id: string, config: TerminalWorktreeConfig | undefined) => void;
setClaudeBusy: (id: string, isBusy: boolean) => void; setClaudeBusy: (id: string, isBusy: boolean) => void;
setPendingClaudeResume: (id: string, pending: boolean) => void; setPendingClaudeResume: (id: string, pending: boolean) => void;
setClaudeNamedOnce: (id: string, named: boolean) => void;
clearAllTerminals: () => void; clearAllTerminals: () => void;
setHasRestoredSessions: (value: boolean) => void; setHasRestoredSessions: (value: boolean) => void;
reorderTerminals: (activeId: string, overId: string) => void; reorderTerminals: (activeId: string, overId: string) => void;
@@ -316,8 +318,9 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
...t, ...t,
isClaudeMode, isClaudeMode,
status: isClaudeMode ? 'claude-active' : 'running', status: isClaudeMode ? 'claude-active' : 'running',
// Reset busy state when leaving Claude mode // Reset busy state and naming flag when leaving Claude mode
isClaudeBusy: isClaudeMode ? t.isClaudeBusy : undefined isClaudeBusy: isClaudeMode ? t.isClaudeBusy : undefined,
claudeNamedOnce: isClaudeMode ? t.claudeNamedOnce : undefined
} }
: t : t
), ),
@@ -364,6 +367,14 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
})); }));
}, },
setClaudeNamedOnce: (id: string, named: boolean) => {
set((state) => ({
terminals: state.terminals.map((t) =>
t.id === id ? { ...t, claudeNamedOnce: named } : t
),
}));
},
clearAllTerminals: () => { clearAllTerminals: () => {
set({ terminals: [], activeTerminalId: null, hasRestoredSessions: false }); set({ terminals: [], activeTerminalId: null, hasRestoredSessions: false });
}, },
+3 -1
View File
@@ -57,7 +57,9 @@ export const DEFAULT_APP_SETTINGS = {
// Language preference (default to English) // Language preference (default to English)
language: 'en' as const, language: 'en' as const,
// Anonymous error reporting (Sentry) - enabled by default to help improve the app // Anonymous error reporting (Sentry) - enabled by default to help improve the app
sentryEnabled: true sentryEnabled: true,
// Auto-name Claude terminals based on initial message (enabled by default)
autoNameClaudeTerminals: true
}; };
// ============================================ // ============================================
@@ -262,6 +262,10 @@
"notInstalled": "Not installed", "notInstalled": "Not installed",
"detectedSummary": "Detected on your system:", "detectedSummary": "Detected on your system:",
"noToolsDetected": "No additional tools detected (VS Code and system terminal will be used)", "noToolsDetected": "No additional tools detected (VS Code and system terminal will be used)",
"autoNameClaude": {
"label": "Auto-name Claude terminals",
"description": "Use AI to generate a descriptive name for Claude terminals based on your first message"
},
"yoloMode": { "yoloMode": {
"label": "YOLO Mode", "label": "YOLO Mode",
"description": "Start Claude with --dangerously-skip-permissions flag, bypassing all safety prompts. Use with extreme caution.", "description": "Start Claude with --dangerously-skip-permissions flag, bypassing all safety prompts. Use with extreme caution.",
@@ -262,6 +262,10 @@
"notInstalled": "Non installé", "notInstalled": "Non installé",
"detectedSummary": "Détecté sur votre système :", "detectedSummary": "Détecté sur votre système :",
"noToolsDetected": "Aucun outil supplémentaire détecté (VS Code et le terminal système seront utilisés)", "noToolsDetected": "Aucun outil supplémentaire détecté (VS Code et le terminal système seront utilisés)",
"autoNameClaude": {
"label": "Nommer automatiquement les terminaux Claude",
"description": "Utiliser l'IA pour générer un nom descriptif pour les terminaux Claude basé sur votre premier message"
},
"yoloMode": { "yoloMode": {
"label": "Mode YOLO", "label": "Mode YOLO",
"description": "Démarrer Claude avec le flag --dangerously-skip-permissions, contournant toutes les invites de sécurité. À utiliser avec une extrême prudence.", "description": "Démarrer Claude avec le flag --dangerously-skip-permissions, contournant toutes les invites de sécurité. À utiliser avec une extrême prudence.",
@@ -283,6 +283,8 @@ export interface AppSettings {
dangerouslySkipPermissions?: boolean; dangerouslySkipPermissions?: boolean;
// Anonymous error reporting (Sentry) - enabled by default to help improve the app // Anonymous error reporting (Sentry) - enabled by default to help improve the app
sentryEnabled?: boolean; sentryEnabled?: boolean;
// Auto-name Claude terminals based on initial message (only triggers once per session)
autoNameClaudeTerminals?: boolean;
} }
// Auto-Claude Source Environment Configuration (for auto-claude repo .env) // Auto-Claude Source Environment Configuration (for auto-claude repo .env)