implement keyboard shortcuts for tab management and adjust layout styles in App and TabBar components

This commit is contained in:
tusharmagar 2026-02-18 11:22:47 +05:30
parent 087e809d4c
commit 09288571aa
2 changed files with 99 additions and 47 deletions

View file

@ -585,7 +585,7 @@ function ContentHeader({
return (
<header
className={cn(
"titlebar-drag-region flex h-10 shrink-0 items-center gap-2 border-b border-border px-3 bg-sidebar transition-[padding] duration-200 ease-linear",
"titlebar-drag-region flex h-10 shrink-0 items-stretch border-b border-border px-3 bg-sidebar transition-[padding] duration-200 ease-linear overflow-hidden",
// When the sidebar is collapsed the content area shifts left, so we need enough left padding
// to avoid overlapping the fixed traffic-lights/toggle/back/forward controls.
isCollapsed && !collapsedLeftPaddingPx && "pl-[168px]"
@ -593,7 +593,7 @@ function ContentHeader({
style={isCollapsed && collapsedLeftPaddingPx ? { paddingLeft: collapsedLeftPaddingPx } : undefined}
>
{!isCollapsed && onNavigateBack && onNavigateForward ? (
<div className="titlebar-no-drag flex items-center gap-1">
<div className="titlebar-no-drag flex items-center gap-1 pr-2 shrink-0">
<button
type="button"
onClick={onNavigateBack}
@ -2040,6 +2040,61 @@ function App() {
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
// Keyboard shortcuts for tab management
useEffect(() => {
const handleTabKeyDown = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey
if (!mod) return
// Cmd+W — close active tab
if (e.key === 'w') {
e.preventDefault()
if (activeSection === 'knowledge' && activeFileTabId) {
closeFileTab(activeFileTabId)
} else if (activeSection === 'tasks') {
closeChatTab(activeChatTabId)
}
return
}
// Cmd+1..9 — switch to tab N (Cmd+9 always goes to last tab)
if (/^[1-9]$/.test(e.key)) {
e.preventDefault()
const n = parseInt(e.key, 10)
if (activeSection === 'knowledge') {
const idx = e.key === '9' ? fileTabs.length - 1 : n - 1
const tab = fileTabs[idx]
if (tab) switchFileTab(tab.id)
} else if (activeSection === 'tasks') {
const idx = e.key === '9' ? chatTabs.length - 1 : n - 1
const tab = chatTabs[idx]
if (tab) switchChatTab(tab.id)
}
return
}
// Cmd+Shift+] — next tab, Cmd+Shift+[ — previous tab
if (e.shiftKey && (e.key === ']' || e.key === '[')) {
e.preventDefault()
const direction = e.key === ']' ? 1 : -1
if (activeSection === 'knowledge') {
const currentIdx = fileTabs.findIndex(t => t.id === activeFileTabId)
if (currentIdx === -1) return
const nextIdx = (currentIdx + direction + fileTabs.length) % fileTabs.length
switchFileTab(fileTabs[nextIdx].id)
} else if (activeSection === 'tasks') {
const currentIdx = chatTabs.findIndex(t => t.id === activeChatTabId)
if (currentIdx === -1) return
const nextIdx = (currentIdx + direction + chatTabs.length) % chatTabs.length
switchChatTab(chatTabs[nextIdx].id)
}
return
}
}
document.addEventListener('keydown', handleTabKeyDown)
return () => document.removeEventListener('keydown', handleTabKeyDown)
}, [activeSection, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') {
navigateToFile(path)
@ -2499,13 +2554,6 @@ function App() {
const conversationContentClassName = hasConversation
? "mx-auto w-full max-w-4xl pb-28"
: "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0"
const headerTitle = selectedPath
? getBaseName(selectedPath)
: isGraphOpen
? 'Graph View'
: selectedBackgroundTask
? `Background Task: ${selectedBackgroundTask}`
: 'Chat'
const selectedTask = selectedBackgroundTask
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
: null
@ -2585,7 +2633,7 @@ function App() {
canNavigateForward={canNavigateForward}
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
>
{activeSection === 'knowledge' && fileTabs.length > 1 ? (
{activeSection === 'knowledge' && fileTabs.length >= 1 ? (
<TabBar
tabs={fileTabs}
activeTabId={activeFileTabId ?? ''}
@ -2594,7 +2642,7 @@ function App() {
onSwitchTab={switchFileTab}
onCloseTab={closeFileTab}
/>
) : activeSection === 'tasks' && chatTabs.length > 1 ? (
) : (
<TabBar
tabs={chatTabs}
activeTabId={activeChatTabId}
@ -2604,10 +2652,6 @@ function App() {
onSwitchTab={switchChatTab}
onCloseTab={closeChatTab}
/>
) : (
<span className="text-sm font-medium text-muted-foreground flex-1 min-w-0 truncate">
{headerTitle}
</span>
)}
<button
type="button"
@ -2618,7 +2662,7 @@ function App() {
<SearchIcon className="size-4" />
</button>
{selectedPath && (
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<div className="flex items-center gap-1 text-xs text-muted-foreground self-center shrink-0 pl-2">
{isSaving ? (
<>
<LoaderIcon className="h-3 w-3 animate-spin" />
@ -2637,7 +2681,7 @@ function App() {
variant="ghost"
size="sm"
onClick={() => { void navigateToView({ type: 'chat', runId }) }}
className="titlebar-no-drag text-foreground"
className="titlebar-no-drag text-foreground self-center shrink-0"
>
Close Graph
</Button>
@ -2646,7 +2690,7 @@ function App() {
<button
type="button"
onClick={handleCloseFullScreenChat}
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors self-center shrink-0"
aria-label="Return to file"
>
<X className="size-5" />
@ -2656,7 +2700,7 @@ function App() {
<button
type="button"
onClick={() => setIsChatSidebarOpen(!isChatSidebarOpen)}
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors -mr-1"
className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors -mr-1 self-center shrink-0"
aria-label="Toggle Chat Sidebar"
>
<PanelRightIcon className="size-5" />

View file

@ -32,44 +32,52 @@ export function TabBar<T>({
onCloseTab,
}: TabBarProps<T>) {
return (
<div className="titlebar-no-drag flex flex-1 items-center gap-0 overflow-x-auto min-w-0">
{tabs.map((tab) => {
<div className="titlebar-no-drag flex flex-1 self-stretch min-w-0 overflow-hidden">
{tabs.map((tab, index) => {
const tabId = getTabId(tab)
const isActive = tabId === activeTabId
const processing = isProcessing?.(tab) ?? false
const title = getTabTitle(tab)
return (
<button
key={tabId}
type="button"
onClick={() => onSwitchTab(tabId)}
className={cn(
"group/tab relative flex items-center gap-1.5 px-3 h-full text-xs max-w-[180px] min-w-[80px] transition-colors",
isActive
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
<React.Fragment key={tabId}>
{index > 0 && (
<div className="self-stretch w-px bg-border/70 shrink-0" aria-hidden="true" />
)}
>
{processing && (
<span className="size-1.5 shrink-0 rounded-full bg-emerald-500 animate-pulse" />
)}
<span className="truncate flex-1 text-left">{title}</span>
<span
role="button"
<button
type="button"
onClick={() => onSwitchTab(tabId)}
className={cn(
"shrink-0 flex items-center justify-center rounded-sm p-0.5 hover:bg-foreground/10 transition-colors",
isActive ? "opacity-60 hover:opacity-100" : "opacity-0 group-hover/tab:opacity-60 hover:!opacity-100"
"group/tab relative flex items-center gap-1.5 px-3 self-stretch text-xs min-w-0 max-w-[220px] transition-colors",
isActive
? "bg-background text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)}
onClick={(e) => {
e.stopPropagation()
onCloseTab(tabId)
}}
aria-label="Close tab"
style={{ flex: '1 1 0px' }}
>
<X className="size-3" />
</span>
</button>
{processing && (
<span className="size-1.5 shrink-0 rounded-full bg-emerald-500 animate-pulse" />
)}
<span className="truncate flex-1 text-left">{title}</span>
{tabs.length > 1 && (
<span
role="button"
className="shrink-0 flex items-center justify-center rounded-sm p-0.5 opacity-0 group-hover/tab:opacity-60 hover:opacity-100! hover:bg-foreground/10 transition-all"
onClick={(e) => {
e.stopPropagation()
onCloseTab(tabId)
}}
aria-label="Close tab"
>
<X className="size-3" />
</span>
)}
</button>
{/* Right edge divider after last tab to close off the section */}
{index === tabs.length - 1 && (
<div className="self-stretch w-px bg-border/70 shrink-0" aria-hidden="true" />
)}
</React.Fragment>
)
})}
</div>