Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions src/__tests__/renderer/components/TabBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5645,3 +5645,267 @@ describe('Performance: Many file tabs (10+)', () => {
expect(inactiveFileTab).toHaveStyle({ marginBottom: '0' });
});
});

describe('TabBar description section', () => {
const mockOnTabSelect = vi.fn();
const mockOnTabClose = vi.fn();
const mockOnNewTab = vi.fn();
const mockOnUpdateTabDescription = vi.fn();
const mockOnTabStar = vi.fn();
const mockOnRequestRename = vi.fn();

const mockThemeDesc: Theme = {
id: 'test-theme',
name: 'Test Theme',
mode: 'dark',
colors: {
bgMain: '#1a1a1a',
bgSidebar: '#2a2a2a',
bgActivity: '#3a3a3a',
textMain: '#ffffff',
textDim: '#888888',
accent: '#007acc',
border: '#444444',
error: '#ff4444',
success: '#44ff44',
warning: '#ffaa00',
vibe: '#ff00ff',
agentStatus: '#00ff00',
},
};

beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
Element.prototype.scrollTo = vi.fn();
Element.prototype.scrollIntoView = vi.fn();
Object.assign(navigator, {
clipboard: {
writeText: vi.fn().mockResolvedValue(undefined),
},
});
});

afterEach(() => {
vi.useRealTimers();
});

function openOverlay(tabName: string) {
const tab = screen.getByText(tabName).closest('[data-tab-id]')!;
fireEvent.mouseEnter(tab);
act(() => {
vi.advanceTimersByTime(450);
});
}

it('renders description section when onUpdateTabDescription is provided', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
}),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onTabStar={mockOnTabStar}
onRequestRename={mockOnRequestRename}
onUpdateTabDescription={mockOnUpdateTabDescription}
/>
);

openOverlay('Tab 1');

expect(screen.getByText('Add description...')).toBeInTheDocument();
});

it('does NOT render description section when onUpdateTabDescription is undefined', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
}),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onTabStar={mockOnTabStar}
onRequestRename={mockOnRequestRename}
/>
);

openOverlay('Tab 1');

expect(screen.queryByText('Add description...')).not.toBeInTheDocument();
});

it('shows "Add description..." placeholder when tab has no description', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
}),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onUpdateTabDescription={mockOnUpdateTabDescription}
/>
);

openOverlay('Tab 1');

expect(screen.getByText('Add description...')).toBeInTheDocument();
expect(screen.getByLabelText('Add tab description')).toBeInTheDocument();
});

it('shows existing description text when tab has one', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
description: 'My existing description',
} as any),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onUpdateTabDescription={mockOnUpdateTabDescription}
/>
);

openOverlay('Tab 1');

expect(screen.getByText('My existing description')).toBeInTheDocument();
expect(screen.getByLabelText('Edit tab description')).toBeInTheDocument();
});

it('clicking description area enters edit mode (textarea appears)', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
}),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onUpdateTabDescription={mockOnUpdateTabDescription}
/>
);

openOverlay('Tab 1');

const descButton = screen.getByLabelText('Add tab description');
fireEvent.click(descButton);

expect(screen.getByLabelText('Tab description')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Add a description...')).toBeInTheDocument();
});

it('pressing Enter in textarea calls onUpdateTabDescription with trimmed value', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
}),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onUpdateTabDescription={mockOnUpdateTabDescription}
/>
);

openOverlay('Tab 1');

const descButton = screen.getByLabelText('Add tab description');
fireEvent.click(descButton);

const textarea = screen.getByLabelText('Tab description');
fireEvent.change(textarea, { target: { value: ' New description ' } });
fireEvent.keyDown(textarea, { key: 'Enter' });

expect(mockOnUpdateTabDescription).toHaveBeenCalledWith('tab-1', 'New description');
});

it('pressing Escape in textarea exits edit mode without calling handler', () => {
const tabs = [
createTab({
id: 'tab-1',
name: 'Tab 1',
agentSessionId: 'session-1',
description: 'Original description',
} as any),
];

render(
<TabBar
tabs={tabs}
activeTabId="tab-1"
theme={mockThemeDesc}
onTabSelect={mockOnTabSelect}
onTabClose={mockOnTabClose}
onNewTab={mockOnNewTab}
onUpdateTabDescription={mockOnUpdateTabDescription}
/>
);

openOverlay('Tab 1');

const descButton = screen.getByLabelText('Edit tab description');
fireEvent.click(descButton);

const textarea = screen.getByLabelText('Tab description');
fireEvent.change(textarea, { target: { value: 'Changed text' } });
fireEvent.keyDown(textarea, { key: 'Escape' });

// Should not call the handler
expect(mockOnUpdateTabDescription).not.toHaveBeenCalled();
// Should exit edit mode — textarea gone, button visible
expect(screen.queryByLabelText('Tab description')).not.toBeInTheDocument();
expect(screen.getByText('Original description')).toBeInTheDocument();
});
});
82 changes: 82 additions & 0 deletions src/__tests__/renderer/hooks/useTabHandlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,88 @@ describe('useTabHandlers', () => {
expect(getSession().aiTabs[0].showThinking).toBe('off');
});

it('handleUpdateTabDescription sets description on tab', () => {
const tab = createMockAITab({ id: 'tab-1' });
const { result } = renderWithSession([tab]);
act(() => {
result.current.handleUpdateTabDescription('tab-1', 'My description');
});

const session = getSession();
expect(session.aiTabs[0].description).toBe('My description');
});

it('handleUpdateTabDescription trims whitespace', () => {
const tab = createMockAITab({ id: 'tab-1' });
const { result } = renderWithSession([tab]);
act(() => {
result.current.handleUpdateTabDescription('tab-1', ' spaces around ');
});

const session = getSession();
expect(session.aiTabs[0].description).toBe('spaces around');
});

it('handleUpdateTabDescription sets undefined for empty string', () => {
const tab = createMockAITab({ id: 'tab-1', description: 'existing' } as any);
const { result } = renderWithSession([tab]);
act(() => {
result.current.handleUpdateTabDescription('tab-1', '');
});

const session = getSession();
expect(session.aiTabs[0].description).toBeUndefined();
});

it('handleUpdateTabDescription sets undefined for whitespace-only string', () => {
const tab = createMockAITab({ id: 'tab-1', description: 'existing' } as any);
const { result } = renderWithSession([tab]);
act(() => {
result.current.handleUpdateTabDescription('tab-1', ' ');
});

const session = getSession();
expect(session.aiTabs[0].description).toBeUndefined();
});

it('handleUpdateTabDescription does not modify tabs in non-active sessions', () => {
const tab = createMockAITab({ id: 'tab-1' });
setupSessionWithTabs([tab]);

// Add a second non-active session with its own tab
const otherSession = createMockSession({
id: 'other-session',
aiTabs: [createMockAITab({ id: 'other-tab' })],
activeTabId: 'other-tab',
unifiedTabOrder: [{ type: 'ai', id: 'other-tab' }],
});
useSessionStore.setState((prev: any) => ({
sessions: [...prev.sessions, otherSession],
}));

const { result } = renderHook(() => useTabHandlers());
act(() => {
result.current.handleUpdateTabDescription('other-tab', 'should not apply');
});

const state = useSessionStore.getState();
const other = state.sessions.find((s) => s.id === 'other-session')!;
expect(other.aiTabs[0].description).toBeUndefined();
});

it('handleUpdateTabDescription does not modify other tabs in the same session', () => {
const tab1 = createMockAITab({ id: 'tab-1' });
const tab2 = createMockAITab({ id: 'tab-2', description: 'original' } as any);
const { result } = renderWithSession([tab1, tab2]);
act(() => {
result.current.handleUpdateTabDescription('tab-1', 'New description');
});

const session = getSession();
expect(session.aiTabs[0].description).toBe('New description');
expect(session.aiTabs[1].description).toBe('original');
});

it('handleUpdateTabByClaudeSessionId updates tab by agent session id', () => {
const tab = createMockAITab({
id: 'tab-1',
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ function MaestroConsoleInner() {
handleCloseCurrentTab,
handleRequestTabRename,
handleUpdateTabByClaudeSessionId,
handleUpdateTabDescription,
handleTabStar,
handleTabMarkUnread,
handleToggleTabReadOnlyMode,
Expand Down Expand Up @@ -3212,6 +3213,9 @@ function MaestroConsoleInner() {
handleTabReorder,
handleUnifiedTabReorder,
handleUpdateTabByClaudeSessionId,
handleUpdateTabDescription: encoreFeatures.tabDescription
? handleUpdateTabDescription
: undefined,
handleTabStar,
handleTabMarkUnread,
handleToggleTabReadOnlyMode,
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/components/MainPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ interface MainPanelProps {
onRequestTabRename?: (tabId: string) => void;
onTabReorder?: (fromIndex: number, toIndex: number) => void;
onUnifiedTabReorder?: (fromIndex: number, toIndex: number) => void;
onUpdateTabDescription?: (tabId: string, description: string) => void;
onTabStar?: (tabId: string, starred: boolean) => void;
onTabMarkUnread?: (tabId: string) => void;
onUpdateTabByClaudeSessionId?: (
Expand Down Expand Up @@ -455,6 +456,7 @@ export const MainPanel = React.memo(
onRequestTabRename,
onTabReorder,
onUnifiedTabReorder,
onUpdateTabDescription,
onTabStar,
onTabMarkUnread,
onToggleUnreadFilter,
Expand Down Expand Up @@ -1470,6 +1472,7 @@ export const MainPanel = React.memo(
onRequestRename={onRequestTabRename}
onTabReorder={onTabReorder}
onUnifiedTabReorder={onUnifiedTabReorder}
onUpdateTabDescription={onUpdateTabDescription}
onTabStar={onTabStar}
onTabMarkUnread={onTabMarkUnread}
onMergeWith={onMergeWith}
Expand Down
Loading