fix(terminal): prevent aggressive renaming on Claude invocation (#1147)

* fix(terminal): prevent aggressive renaming on Claude invocation

Add shouldAutoRenameTerminal() helper that only allows renaming when:
- Terminal has default name pattern ("Terminal X")
- Terminal doesn't already have a Claude-related title

This preserves user-customized terminal names and prevents
renaming on every Claude invocation or resume.

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

* test(terminal): update tests for shouldAutoRenameTerminal behavior

Update finalizeClaudeInvoke tests to use default terminal name pattern
("Terminal X") so renaming logic is tested correctly.

Add new tests verifying:
- Terminals already named "Claude" are NOT renamed
- User-customized terminal names are preserved

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-15 23:25:39 +01:00
committed by GitHub
parent 8b269fea90
commit 10bceac9cb
2 changed files with 106 additions and 24 deletions
@@ -502,9 +502,10 @@ describe('claude-integration-handler - Helper Functions', () => {
});
describe('finalizeClaudeInvoke', () => {
it('should set terminal title to "Claude" for default profile', async () => {
it('should set terminal title to "Claude" for default profile when terminal has default name', async () => {
const { finalizeClaudeInvoke } = await import('../claude-integration-handler');
const terminal = createMockTerminal();
// Use a default terminal name pattern so renaming logic kicks in
const terminal = createMockTerminal({ title: 'Terminal 1' });
const mockWindow = {
webContents: { send: vi.fn() }
};
@@ -523,7 +524,8 @@ describe('claude-integration-handler - Helper Functions', () => {
it('should set terminal title to "Claude (ProfileName)" for non-default profile', async () => {
const { finalizeClaudeInvoke } = await import('../claude-integration-handler');
const terminal = createMockTerminal();
// Use a default terminal name pattern so renaming logic kicks in
const terminal = createMockTerminal({ title: 'Terminal 2' });
const mockWindow = {
webContents: { send: vi.fn() }
};
@@ -540,9 +542,10 @@ describe('claude-integration-handler - Helper Functions', () => {
expect(terminal.title).toBe('Claude (Work Profile)');
});
it('should send IPC message to renderer', async () => {
it('should send IPC message to renderer when terminal has default name', async () => {
const { finalizeClaudeInvoke } = await import('../claude-integration-handler');
const terminal = createMockTerminal();
// Use a default terminal name pattern so renaming logic kicks in
const terminal = createMockTerminal({ title: 'Terminal 3' });
const mockSend = vi.fn();
const mockWindow = {
webContents: { send: mockSend }
@@ -564,6 +567,54 @@ describe('claude-integration-handler - Helper Functions', () => {
);
});
it('should NOT rename terminal when already named Claude', async () => {
const { finalizeClaudeInvoke } = await import('../claude-integration-handler');
// Terminal already has Claude title - should NOT be renamed
const terminal = createMockTerminal({ title: 'Claude' });
const mockSend = vi.fn();
const mockWindow = {
webContents: { send: mockSend }
};
finalizeClaudeInvoke(
terminal,
{ name: 'Work Profile', isDefault: false },
'/tmp/project',
Date.now(),
() => mockWindow as any,
vi.fn()
);
// Title should remain unchanged
expect(terminal.title).toBe('Claude');
// No IPC message should be sent for title change
expect(mockSend).not.toHaveBeenCalled();
});
it('should NOT rename terminal with user-customized name', async () => {
const { finalizeClaudeInvoke } = await import('../claude-integration-handler');
// User has customized the terminal name - should NOT be renamed
const terminal = createMockTerminal({ title: 'My Custom Terminal' });
const mockSend = vi.fn();
const mockWindow = {
webContents: { send: mockSend }
};
finalizeClaudeInvoke(
terminal,
undefined,
'/tmp/project',
Date.now(),
() => mockWindow as any,
vi.fn()
);
// Title should remain unchanged
expect(terminal.title).toBe('My Custom Terminal');
// No IPC message should be sent for title change
expect(mockSend).not.toHaveBeenCalled();
});
it('should persist session when terminal has projectPath', async () => {
const { finalizeClaudeInvoke } = await import('../claude-integration-handler');
const terminal = createMockTerminal({ projectPath: '/tmp/project' });
@@ -102,6 +102,27 @@ interface ProfileInfo {
isDefault?: boolean;
}
/**
* Check if a terminal should be auto-renamed when Claude is invoked.
* Returns false if:
* - Terminal already has a Claude-related title (already renamed)
* - Terminal has a user-customized name (not "Terminal X" pattern)
*
* This prevents aggressive renaming on every Claude invocation and
* preserves user-customized terminal names.
*/
function shouldAutoRenameTerminal(currentTitle: string): boolean {
// Already has Claude title - don't rename again
if (currentTitle === 'Claude' || currentTitle.startsWith('Claude (')) {
return false;
}
// Check if it's a default terminal name (Terminal 1, Terminal 2, etc.)
// Only these can be auto-renamed on first Claude invocation
const defaultNamePattern = /^Terminal \d+$/;
return defaultNamePattern.test(currentTitle);
}
/**
* Callback type for session capture
*/
@@ -139,16 +160,19 @@ export function finalizeClaudeInvoke(
getWindow: WindowGetter,
onSessionCapture: SessionCaptureCallback
): void {
// Set terminal title based on profile
const title = activeProfile && !activeProfile.isDefault
? `Claude (${activeProfile.name})`
: 'Claude';
terminal.title = title;
// Only auto-rename if terminal has default name (first Claude invocation)
// This preserves user-customized names and prevents renaming on every invocation
if (shouldAutoRenameTerminal(terminal.title)) {
const title = activeProfile && !activeProfile.isDefault
? `Claude (${activeProfile.name})`
: 'Claude';
terminal.title = title;
// Notify renderer of title change
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, terminal.id, title);
// Notify renderer of title change
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, terminal.id, title);
}
}
// Persist session if project path is available
@@ -528,14 +552,17 @@ export function resumeClaude(
terminal.pty.write(`${command}\r`);
// Update terminal title in main process and notify renderer
terminal.title = 'Claude';
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, terminal.id, 'Claude');
// Only auto-rename if terminal has default name
// This preserves user-customized names and prevents renaming on every resume
if (shouldAutoRenameTerminal(terminal.title)) {
terminal.title = 'Claude';
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, terminal.id, 'Claude');
}
}
// Persist session with updated title
// Persist session
if (terminal.projectPath) {
SessionHandler.persistSession(terminal);
}
@@ -702,10 +729,14 @@ export async function resumeClaudeAsync(
terminal.pty.write(`${command}\r`);
terminal.title = 'Claude';
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, terminal.id, 'Claude');
// Only auto-rename if terminal has default name
// This preserves user-customized names and prevents renaming on every resume
if (shouldAutoRenameTerminal(terminal.title)) {
terminal.title = 'Claude';
const win = getWindow();
if (win) {
win.webContents.send(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, terminal.id, 'Claude');
}
}
if (terminal.projectPath) {