mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add file tabs to TabSwitcherModal (Cmd+Shift+O)
Updated TabSwitcherModal to display both AI and file preview tabs: - Added fileTabs, activeFileTabId, and onFileTabSelect props - File tabs show in "Open Tabs" mode with AI tabs, sorted alphabetically - Each file tab displays: filename, extension badge with type-specific coloring, file path, "File" indicator, active/unsaved indicators - Search works across file names, extensions, and paths - Updated AppModals and App.tsx to pass file tabs and handlers - Added 9 new tests (total 85 tests pass)
This commit is contained in:
@@ -22,6 +22,7 @@ import type { Theme, AITab } from '../../../renderer/types';
|
||||
vi.mock('lucide-react', () => ({
|
||||
Search: () => <svg data-testid="search-icon" />,
|
||||
Star: () => <svg data-testid="star-icon" />,
|
||||
FileText: () => <svg data-testid="file-text-icon" />,
|
||||
}));
|
||||
|
||||
// Create a test theme
|
||||
@@ -2104,4 +2105,262 @@ describe('TabSwitcherModal', () => {
|
||||
expect(screen.getByText('1 sessions')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('file tab support', () => {
|
||||
// Helper to create a test file tab
|
||||
const createTestFileTab = (overrides: Partial<import('../../../renderer/types').FilePreviewTab> = {}) => ({
|
||||
id: `file-tab-${Math.random().toString(36).substr(2, 9)}`,
|
||||
path: '/test/project/src/example.ts',
|
||||
name: 'example',
|
||||
extension: '.ts',
|
||||
content: 'export const example = 1;',
|
||||
scrollTop: 0,
|
||||
searchQuery: '',
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('includes file tabs in Open Tabs count', () => {
|
||||
const aiTabs = [createTestTab({ name: 'AI Tab' })];
|
||||
const fileTabs = [
|
||||
createTestFileTab({ name: 'file1', extension: '.ts' }),
|
||||
createTestFileTab({ name: 'file2', extension: '.md' }),
|
||||
];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={aiTabs}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId={aiTabs[0].id}
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should show 3 total tabs (1 AI + 2 file)
|
||||
expect(screen.getByText('Open Tabs (3)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders file tabs with extension badge', () => {
|
||||
const fileTabs = [createTestFileTab({ name: 'example', extension: '.ts' })];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('example')).toBeInTheDocument();
|
||||
expect(screen.getByText('.ts')).toBeInTheDocument();
|
||||
expect(screen.getByText('File')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onFileTabSelect when clicking a file tab', () => {
|
||||
const fileTabs = [createTestFileTab({ name: 'myfile' })];
|
||||
const onFileTabSelect = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onFileTabSelect={onFileTabSelect}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('myfile'));
|
||||
|
||||
expect(onFileTabSelect).toHaveBeenCalledWith(fileTabs[0].id);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('filters file tabs by search query', () => {
|
||||
const fileTabs = [
|
||||
createTestFileTab({ name: 'component', extension: '.tsx' }),
|
||||
createTestFileTab({ name: 'utils', extension: '.ts' }),
|
||||
];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByPlaceholderText('Search open tabs...');
|
||||
fireEvent.change(input, { target: { value: 'component' } });
|
||||
|
||||
expect(screen.getByText('component')).toBeInTheDocument();
|
||||
expect(screen.queryByText('utils')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows unsaved indicator for file tabs with edits', () => {
|
||||
const fileTabs = [
|
||||
createTestFileTab({ name: 'unsaved', editContent: 'some edited content' }),
|
||||
];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Unsaved indicator should be shown
|
||||
expect(screen.getByText('●')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows active indicator for active file tab', () => {
|
||||
const fileTabs = [createTestFileTab({ id: 'active-file-tab' })];
|
||||
|
||||
const { container } = renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
activeFileTabId="active-file-tab"
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// Active file tab should show a green dot instead of file icon
|
||||
const dots = container.querySelectorAll('.w-2.h-2.rounded-full');
|
||||
const greenDot = Array.from(dots).find((d) => {
|
||||
const style = d.getAttribute('style') || '';
|
||||
return style.includes('rgb(137, 209, 133)') || style.includes(theme.colors.success);
|
||||
});
|
||||
expect(greenDot).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sorts file tabs alphabetically with AI tabs', () => {
|
||||
const aiTabs = [createTestTab({ name: 'Beta AI' })];
|
||||
const fileTabs = [
|
||||
createTestFileTab({ name: 'Zeta' }),
|
||||
createTestFileTab({ name: 'Alpha' }),
|
||||
];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={aiTabs}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId={aiTabs[0].id}
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
const buttons = screen
|
||||
.getAllByRole('button')
|
||||
.filter(
|
||||
(b) =>
|
||||
b.textContent?.includes('Alpha') ||
|
||||
b.textContent?.includes('Beta AI') ||
|
||||
b.textContent?.includes('Zeta')
|
||||
);
|
||||
|
||||
// Should be sorted: Alpha (file), Beta AI (ai), Zeta (file)
|
||||
expect(buttons[0]).toHaveTextContent('Alpha');
|
||||
expect(buttons[1]).toHaveTextContent('Beta AI');
|
||||
expect(buttons[2]).toHaveTextContent('Zeta');
|
||||
});
|
||||
|
||||
it('shows file extension badge for non-selected file tabs', () => {
|
||||
// Create two file tabs - the second one won't be selected by default
|
||||
const fileTabs = [
|
||||
createTestFileTab({ name: 'aaa', extension: '.ts' }),
|
||||
createTestFileTab({ name: 'readme', extension: '.md' }),
|
||||
];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
// The .md extension should be present
|
||||
const mdBadge = screen.getByText('.md');
|
||||
expect(mdBadge).toBeInTheDocument();
|
||||
|
||||
// The .ts extension should also be present
|
||||
const tsBadge = screen.getByText('.ts');
|
||||
expect(tsBadge).toBeInTheDocument();
|
||||
|
||||
// Check second file tab's (readme.md) extension has green-ish color
|
||||
// (first item is selected, so it has a different color)
|
||||
const mdStyle = mdBadge.getAttribute('style') || '';
|
||||
expect(mdStyle).toContain('background-color');
|
||||
// Green color for markdown files
|
||||
expect(mdStyle).toMatch(/34,\s*197,\s*94/);
|
||||
});
|
||||
|
||||
it('renders file path in file tab item', () => {
|
||||
const fileTabs = [
|
||||
createTestFileTab({
|
||||
name: 'example',
|
||||
extension: '.ts',
|
||||
path: '/project/src/components/example.ts',
|
||||
}),
|
||||
];
|
||||
|
||||
renderWithLayerStack(
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={[]}
|
||||
fileTabs={fileTabs}
|
||||
activeTabId=""
|
||||
projectRoot="/test"
|
||||
onTabSelect={vi.fn()}
|
||||
onNamedSessionSelect={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('/project/src/components/example.ts')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12179,8 +12179,21 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
const handleUtilityTabSelect = useCallback(
|
||||
(tabId: string) => {
|
||||
if (!activeSession) return;
|
||||
// Clear activeFileTabId when selecting an AI tab
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => (s.id === activeSession.id ? { ...s, activeTabId: tabId } : s))
|
||||
prev.map((s) =>
|
||||
s.id === activeSession.id ? { ...s, activeTabId: tabId, activeFileTabId: null } : s
|
||||
)
|
||||
);
|
||||
},
|
||||
[activeSession]
|
||||
);
|
||||
const handleUtilityFileTabSelect = useCallback(
|
||||
(tabId: string) => {
|
||||
if (!activeSession) return;
|
||||
// Set activeFileTabId, keep activeTabId as-is (for when returning to AI tabs)
|
||||
setSessions((prev) =>
|
||||
prev.map((s) => (s.id === activeSession.id ? { ...s, activeFileTabId: tabId } : s))
|
||||
);
|
||||
},
|
||||
[activeSession]
|
||||
@@ -13715,6 +13728,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
tabSwitcherOpen={tabSwitcherOpen}
|
||||
onCloseTabSwitcher={handleCloseTabSwitcher}
|
||||
onTabSelect={handleUtilityTabSelect}
|
||||
onFileTabSelect={handleUtilityFileTabSelect}
|
||||
onNamedSessionSelect={handleNamedSessionSelect}
|
||||
fuzzyFileSearchOpen={fuzzyFileSearchOpen}
|
||||
filteredFileTree={filteredFileTree}
|
||||
|
||||
@@ -867,6 +867,7 @@ export interface AppUtilityModalsProps {
|
||||
tabSwitcherOpen: boolean;
|
||||
onCloseTabSwitcher: () => void;
|
||||
onTabSelect: (tabId: string) => void;
|
||||
onFileTabSelect?: (tabId: string) => void;
|
||||
onNamedSessionSelect: (
|
||||
agentSessionId: string,
|
||||
projectPath: string,
|
||||
@@ -1048,6 +1049,7 @@ export function AppUtilityModals({
|
||||
tabSwitcherOpen,
|
||||
onCloseTabSwitcher,
|
||||
onTabSelect,
|
||||
onFileTabSelect,
|
||||
onNamedSessionSelect,
|
||||
// FileSearchModal
|
||||
fuzzyFileSearchOpen,
|
||||
@@ -1243,11 +1245,14 @@ export function AppUtilityModals({
|
||||
<TabSwitcherModal
|
||||
theme={theme}
|
||||
tabs={activeSession.aiTabs}
|
||||
fileTabs={activeSession.filePreviewTabs}
|
||||
activeTabId={activeSession.activeTabId}
|
||||
activeFileTabId={activeSession.activeFileTabId}
|
||||
projectRoot={activeSession.projectRoot}
|
||||
agentId={activeSession.toolType}
|
||||
shortcut={tabShortcuts.tabSwitcher}
|
||||
onTabSelect={onTabSelect}
|
||||
onFileTabSelect={onFileTabSelect}
|
||||
onNamedSessionSelect={onNamedSessionSelect}
|
||||
onClose={onCloseTabSwitcher}
|
||||
/>
|
||||
@@ -1928,6 +1933,7 @@ export interface AppModalsProps {
|
||||
tabSwitcherOpen: boolean;
|
||||
onCloseTabSwitcher: () => void;
|
||||
onTabSelect: (tabId: string) => void;
|
||||
onFileTabSelect?: (tabId: string) => void;
|
||||
onNamedSessionSelect: (
|
||||
agentSessionId: string,
|
||||
projectPath: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { Search, Star } from 'lucide-react';
|
||||
import type { AITab, Theme, Shortcut, ToolType } from '../types';
|
||||
import { Search, Star, FileText } from 'lucide-react';
|
||||
import type { AITab, FilePreviewTab, Theme, Shortcut, ToolType } from '../types';
|
||||
import { fuzzyMatchWithScore } from '../utils/search';
|
||||
import { useLayerStack } from '../contexts/LayerStackContext';
|
||||
import { useListNavigation } from '../hooks';
|
||||
@@ -21,16 +21,25 @@ interface NamedSession {
|
||||
}
|
||||
|
||||
/** Union type for items in the list */
|
||||
type ListItem = { type: 'open'; tab: AITab } | { type: 'named'; session: NamedSession };
|
||||
type ListItem =
|
||||
| { type: 'open'; tab: AITab }
|
||||
| { type: 'file'; tab: FilePreviewTab }
|
||||
| { type: 'named'; session: NamedSession };
|
||||
|
||||
interface TabSwitcherModalProps {
|
||||
theme: Theme;
|
||||
tabs: AITab[];
|
||||
/** File preview tabs to include in "Open Tabs" view */
|
||||
fileTabs?: FilePreviewTab[];
|
||||
activeTabId: string;
|
||||
/** Currently active file tab ID (if a file tab is active) */
|
||||
activeFileTabId?: string | null;
|
||||
projectRoot: string; // The initial project directory (used for Claude session storage)
|
||||
agentId?: string;
|
||||
shortcut?: Shortcut;
|
||||
onTabSelect: (tabId: string) => void;
|
||||
/** Handler to select a file tab */
|
||||
onFileTabSelect?: (tabId: string) => void;
|
||||
onNamedSessionSelect: (
|
||||
agentSessionId: string,
|
||||
projectPath: string,
|
||||
@@ -96,6 +105,37 @@ function getUuidPill(agentSessionId: string | undefined | null): string | null {
|
||||
return agentSessionId.split('-')[0].toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for file extension badge.
|
||||
* Returns a muted color based on file type for visual differentiation.
|
||||
* (Copied from TabBar.tsx for consistency)
|
||||
*/
|
||||
function getExtensionColor(extension: string, theme: Theme): { bg: string; text: string } {
|
||||
const ext = extension.toLowerCase();
|
||||
// TypeScript/JavaScript - blue tones
|
||||
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
||||
return { bg: 'rgba(59, 130, 246, 0.3)', text: 'rgba(147, 197, 253, 0.9)' };
|
||||
}
|
||||
// Markdown/Docs - green tones
|
||||
if (['.md', '.mdx', '.txt', '.rst'].includes(ext)) {
|
||||
return { bg: 'rgba(34, 197, 94, 0.3)', text: 'rgba(134, 239, 172, 0.9)' };
|
||||
}
|
||||
// JSON/Config - yellow tones
|
||||
if (['.json', '.yaml', '.yml', '.toml', '.ini', '.env'].includes(ext)) {
|
||||
return { bg: 'rgba(234, 179, 8, 0.3)', text: 'rgba(253, 224, 71, 0.9)' };
|
||||
}
|
||||
// CSS/Styles - purple tones
|
||||
if (['.css', '.scss', '.sass', '.less', '.styl'].includes(ext)) {
|
||||
return { bg: 'rgba(168, 85, 247, 0.3)', text: 'rgba(216, 180, 254, 0.9)' };
|
||||
}
|
||||
// HTML/Templates - orange tones
|
||||
if (['.html', '.htm', '.xml', '.svg'].includes(ext)) {
|
||||
return { bg: 'rgba(249, 115, 22, 0.3)', text: 'rgba(253, 186, 116, 0.9)' };
|
||||
}
|
||||
// Default - use theme's dim colors
|
||||
return { bg: theme.colors.border, text: theme.colors.textDim };
|
||||
}
|
||||
|
||||
/**
|
||||
* Circular progress gauge component
|
||||
*/
|
||||
@@ -154,18 +194,22 @@ function ContextGauge({
|
||||
type ViewMode = 'open' | 'all-named' | 'starred';
|
||||
|
||||
/**
|
||||
* Tab Switcher Modal - Quick navigation between AI tabs with fuzzy search.
|
||||
* Shows context window consumption, cost, custom name, and UUID pill for each tab.
|
||||
* Supports switching between "Open Tabs" and "All Named" sessions.
|
||||
* Tab Switcher Modal - Quick navigation between AI and file tabs with fuzzy search.
|
||||
* Shows context window consumption, cost, custom name, and UUID pill for AI tabs.
|
||||
* Shows filename, extension badge, and file icon for file tabs.
|
||||
* Supports switching between "Open Tabs", "All Named" sessions, and "Starred".
|
||||
*/
|
||||
export function TabSwitcherModal({
|
||||
theme,
|
||||
tabs,
|
||||
fileTabs = [],
|
||||
activeTabId,
|
||||
activeFileTabId,
|
||||
projectRoot,
|
||||
agentId = 'claude-code',
|
||||
shortcut,
|
||||
onTabSelect,
|
||||
onFileTabSelect,
|
||||
onNamedSessionSelect,
|
||||
onClose,
|
||||
}: TabSwitcherModalProps) {
|
||||
@@ -270,13 +314,37 @@ export function TabSwitcherModal({
|
||||
// Build the list items based on view mode
|
||||
const listItems: ListItem[] = useMemo(() => {
|
||||
if (viewMode === 'open') {
|
||||
// Open tabs mode - show all currently open tabs
|
||||
const sorted = [...tabs].sort((a, b) => {
|
||||
const nameA = getTabDisplayName(a).toLowerCase();
|
||||
const nameB = getTabDisplayName(b).toLowerCase();
|
||||
// Open tabs mode - show all currently open tabs (AI and file tabs)
|
||||
const items: ListItem[] = [];
|
||||
|
||||
// Add AI tabs
|
||||
for (const tab of tabs) {
|
||||
items.push({ type: 'open' as const, tab });
|
||||
}
|
||||
|
||||
// Add file tabs
|
||||
for (const tab of fileTabs) {
|
||||
items.push({ type: 'file' as const, tab });
|
||||
}
|
||||
|
||||
// Sort alphabetically by display name
|
||||
items.sort((a, b) => {
|
||||
const nameA =
|
||||
a.type === 'open'
|
||||
? getTabDisplayName(a.tab).toLowerCase()
|
||||
: a.type === 'file'
|
||||
? a.tab.name.toLowerCase()
|
||||
: '';
|
||||
const nameB =
|
||||
b.type === 'open'
|
||||
? getTabDisplayName(b.tab).toLowerCase()
|
||||
: b.type === 'file'
|
||||
? b.tab.name.toLowerCase()
|
||||
: '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
return sorted.map((tab) => ({ type: 'open' as const, tab }));
|
||||
|
||||
return items;
|
||||
} else if (viewMode === 'starred') {
|
||||
// Starred mode - show all starred sessions (open or closed) for the current project
|
||||
const items: ListItem[] = [];
|
||||
@@ -304,11 +372,15 @@ export function TabSwitcherModal({
|
||||
const nameA =
|
||||
a.type === 'open'
|
||||
? getTabDisplayName(a.tab).toLowerCase()
|
||||
: a.session.sessionName.toLowerCase();
|
||||
: a.type === 'named'
|
||||
? a.session.sessionName.toLowerCase()
|
||||
: '';
|
||||
const nameB =
|
||||
b.type === 'open'
|
||||
? getTabDisplayName(b.tab).toLowerCase()
|
||||
: b.session.sessionName.toLowerCase();
|
||||
: b.type === 'named'
|
||||
? b.session.sessionName.toLowerCase()
|
||||
: '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
@@ -345,17 +417,21 @@ export function TabSwitcherModal({
|
||||
const nameA =
|
||||
a.type === 'open'
|
||||
? getTabDisplayName(a.tab).toLowerCase()
|
||||
: a.session.sessionName.toLowerCase();
|
||||
: a.type === 'named'
|
||||
? a.session.sessionName.toLowerCase()
|
||||
: '';
|
||||
const nameB =
|
||||
b.type === 'open'
|
||||
? getTabDisplayName(b.tab).toLowerCase()
|
||||
: b.session.sessionName.toLowerCase();
|
||||
: b.type === 'named'
|
||||
? b.session.sessionName.toLowerCase()
|
||||
: '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}, [viewMode, tabs, namedSessions, openTabSessionIds, projectRoot]);
|
||||
}, [viewMode, tabs, fileTabs, namedSessions, openTabSessionIds, projectRoot]);
|
||||
|
||||
// Filter items based on search query
|
||||
const filteredItems = useMemo(() => {
|
||||
@@ -366,21 +442,25 @@ export function TabSwitcherModal({
|
||||
// Fuzzy search
|
||||
const results = listItems.map((item) => {
|
||||
let displayName: string;
|
||||
let uuid: string;
|
||||
let searchableId: string;
|
||||
|
||||
if (item.type === 'open') {
|
||||
displayName = getTabDisplayName(item.tab);
|
||||
uuid = item.tab.agentSessionId || '';
|
||||
searchableId = item.tab.agentSessionId || '';
|
||||
} else if (item.type === 'file') {
|
||||
// For file tabs, search by name and extension
|
||||
displayName = item.tab.name;
|
||||
searchableId = item.tab.extension + ' ' + item.tab.path;
|
||||
} else {
|
||||
displayName = item.session.sessionName;
|
||||
uuid = item.session.agentSessionId;
|
||||
searchableId = item.session.agentSessionId;
|
||||
}
|
||||
|
||||
const nameResult = fuzzyMatchWithScore(displayName, search);
|
||||
const uuidResult = fuzzyMatchWithScore(uuid, search);
|
||||
const idResult = fuzzyMatchWithScore(searchableId, search);
|
||||
|
||||
const bestScore = Math.max(nameResult.score, uuidResult.score);
|
||||
const matches = nameResult.matches || uuidResult.matches;
|
||||
const bestScore = Math.max(nameResult.score, idResult.score);
|
||||
const matches = nameResult.matches || idResult.matches;
|
||||
|
||||
return { item, score: bestScore, matches };
|
||||
});
|
||||
@@ -398,6 +478,8 @@ export function TabSwitcherModal({
|
||||
if (item) {
|
||||
if (item.type === 'open') {
|
||||
onTabSelect(item.tab.id);
|
||||
} else if (item.type === 'file') {
|
||||
onFileTabSelect?.(item.tab.id);
|
||||
} else {
|
||||
onNamedSessionSelect(
|
||||
item.session.agentSessionId,
|
||||
@@ -409,7 +491,7 @@ export function TabSwitcherModal({
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[filteredItems, onTabSelect, onNamedSessionSelect, onClose]
|
||||
[filteredItems, onTabSelect, onFileTabSelect, onNamedSessionSelect, onClose]
|
||||
);
|
||||
|
||||
// Use the list navigation hook for keyboard navigation
|
||||
@@ -528,7 +610,7 @@ export function TabSwitcherModal({
|
||||
color: viewMode === 'open' ? theme.colors.accentForeground : theme.colors.textDim,
|
||||
}}
|
||||
>
|
||||
Open Tabs ({tabs.length})
|
||||
Open Tabs ({tabs.length + fileTabs.length})
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('all-named')}
|
||||
@@ -690,6 +772,97 @@ export function TabSwitcherModal({
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
} else if (item.type === 'file') {
|
||||
// File preview tab
|
||||
const { tab } = item;
|
||||
const isActive = tab.id === activeFileTabId;
|
||||
const extColors = getExtensionColor(tab.extension, theme);
|
||||
const hasUnsavedEdits = !!tab.editContent;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
ref={isSelected ? selectedItemRef : null}
|
||||
onClick={() => handleSelectByIndex(i)}
|
||||
className="w-full text-left px-4 py-3 flex items-center gap-3 hover:bg-opacity-10"
|
||||
style={{
|
||||
backgroundColor: isSelected ? theme.colors.accent : 'transparent',
|
||||
color: isSelected ? theme.colors.accentForeground : theme.colors.textMain,
|
||||
}}
|
||||
>
|
||||
{/* Number Badge */}
|
||||
{showNumber ? (
|
||||
<div
|
||||
className="flex-shrink-0 w-5 h-5 rounded flex items-center justify-center text-xs font-bold"
|
||||
style={{ backgroundColor: theme.colors.bgMain, color: theme.colors.textDim }}
|
||||
>
|
||||
{numberBadge}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-shrink-0 w-5 h-5" />
|
||||
)}
|
||||
|
||||
{/* File Icon - shows active indicator or file icon */}
|
||||
<div className="flex-shrink-0 w-4 h-4 flex items-center justify-center">
|
||||
{isActive ? (
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ backgroundColor: theme.colors.success }}
|
||||
/>
|
||||
) : (
|
||||
<FileText
|
||||
className="w-3.5 h-3.5"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File Info */}
|
||||
<div className="flex flex-col flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium truncate">{tab.name}</span>
|
||||
{/* Extension badge */}
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded font-mono flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: isSelected
|
||||
? 'rgba(255,255,255,0.2)'
|
||||
: extColors.bg,
|
||||
color: isSelected
|
||||
? theme.colors.accentForeground
|
||||
: extColors.text,
|
||||
}}
|
||||
>
|
||||
{tab.extension}
|
||||
</span>
|
||||
{/* Unsaved indicator */}
|
||||
{hasUnsavedEdits && (
|
||||
<span
|
||||
className="text-[10px] opacity-80"
|
||||
style={{ color: theme.colors.warning }}
|
||||
>
|
||||
●
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{/* File path (truncated) */}
|
||||
<div className="flex items-center gap-3 text-[10px] opacity-60 truncate">
|
||||
<span className="truncate">{tab.path}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File indicator instead of gauge */}
|
||||
<div
|
||||
className="flex-shrink-0 text-[10px] px-2 py-1 rounded"
|
||||
style={{
|
||||
backgroundColor: isSelected ? 'rgba(255,255,255,0.2)' : theme.colors.bgMain,
|
||||
color: isSelected ? theme.colors.accentForeground : theme.colors.textDim,
|
||||
}}
|
||||
>
|
||||
File
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
// Named session (not open)
|
||||
const { session } = item;
|
||||
|
||||
Reference in New Issue
Block a user