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 ( return (
<header <header
className={cn( 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 // 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. // to avoid overlapping the fixed traffic-lights/toggle/back/forward controls.
isCollapsed && !collapsedLeftPaddingPx && "pl-[168px]" isCollapsed && !collapsedLeftPaddingPx && "pl-[168px]"
@ -593,7 +593,7 @@ function ContentHeader({
style={isCollapsed && collapsedLeftPaddingPx ? { paddingLeft: collapsedLeftPaddingPx } : undefined} style={isCollapsed && collapsedLeftPaddingPx ? { paddingLeft: collapsedLeftPaddingPx } : undefined}
> >
{!isCollapsed && onNavigateBack && onNavigateForward ? ( {!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 <button
type="button" type="button"
onClick={onNavigateBack} onClick={onNavigateBack}
@ -2040,6 +2040,61 @@ function App() {
return () => document.removeEventListener('keydown', handleKeyDown) 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') => { const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') { if (kind === 'file') {
navigateToFile(path) navigateToFile(path)
@ -2499,13 +2554,6 @@ function App() {
const conversationContentClassName = hasConversation const conversationContentClassName = hasConversation
? "mx-auto w-full max-w-4xl pb-28" ? "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" : "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 const selectedTask = selectedBackgroundTask
? backgroundTasks.find(t => t.name === selectedBackgroundTask) ? backgroundTasks.find(t => t.name === selectedBackgroundTask)
: null : null
@ -2585,7 +2633,7 @@ function App() {
canNavigateForward={canNavigateForward} canNavigateForward={canNavigateForward}
collapsedLeftPaddingPx={collapsedLeftPaddingPx} collapsedLeftPaddingPx={collapsedLeftPaddingPx}
> >
{activeSection === 'knowledge' && fileTabs.length > 1 ? ( {activeSection === 'knowledge' && fileTabs.length >= 1 ? (
<TabBar <TabBar
tabs={fileTabs} tabs={fileTabs}
activeTabId={activeFileTabId ?? ''} activeTabId={activeFileTabId ?? ''}
@ -2594,7 +2642,7 @@ function App() {
onSwitchTab={switchFileTab} onSwitchTab={switchFileTab}
onCloseTab={closeFileTab} onCloseTab={closeFileTab}
/> />
) : activeSection === 'tasks' && chatTabs.length > 1 ? ( ) : (
<TabBar <TabBar
tabs={chatTabs} tabs={chatTabs}
activeTabId={activeChatTabId} activeTabId={activeChatTabId}
@ -2604,10 +2652,6 @@ function App() {
onSwitchTab={switchChatTab} onSwitchTab={switchChatTab}
onCloseTab={closeChatTab} onCloseTab={closeChatTab}
/> />
) : (
<span className="text-sm font-medium text-muted-foreground flex-1 min-w-0 truncate">
{headerTitle}
</span>
)} )}
<button <button
type="button" type="button"
@ -2618,7 +2662,7 @@ function App() {
<SearchIcon className="size-4" /> <SearchIcon className="size-4" />
</button> </button>
{selectedPath && ( {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 ? ( {isSaving ? (
<> <>
<LoaderIcon className="h-3 w-3 animate-spin" /> <LoaderIcon className="h-3 w-3 animate-spin" />
@ -2637,7 +2681,7 @@ function App() {
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => { void navigateToView({ type: 'chat', runId }) }} onClick={() => { void navigateToView({ type: 'chat', runId }) }}
className="titlebar-no-drag text-foreground" className="titlebar-no-drag text-foreground self-center shrink-0"
> >
Close Graph Close Graph
</Button> </Button>
@ -2646,7 +2690,7 @@ function App() {
<button <button
type="button" type="button"
onClick={handleCloseFullScreenChat} 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" aria-label="Return to file"
> >
<X className="size-5" /> <X className="size-5" />
@ -2656,7 +2700,7 @@ function App() {
<button <button
type="button" type="button"
onClick={() => setIsChatSidebarOpen(!isChatSidebarOpen)} 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" aria-label="Toggle Chat Sidebar"
> >
<PanelRightIcon className="size-5" /> <PanelRightIcon className="size-5" />

View file

@ -32,35 +32,37 @@ export function TabBar<T>({
onCloseTab, onCloseTab,
}: TabBarProps<T>) { }: TabBarProps<T>) {
return ( return (
<div className="titlebar-no-drag flex flex-1 items-center gap-0 overflow-x-auto min-w-0"> <div className="titlebar-no-drag flex flex-1 self-stretch min-w-0 overflow-hidden">
{tabs.map((tab) => { {tabs.map((tab, index) => {
const tabId = getTabId(tab) const tabId = getTabId(tab)
const isActive = tabId === activeTabId const isActive = tabId === activeTabId
const processing = isProcessing?.(tab) ?? false const processing = isProcessing?.(tab) ?? false
const title = getTabTitle(tab) const title = getTabTitle(tab)
return ( return (
<React.Fragment key={tabId}>
{index > 0 && (
<div className="self-stretch w-px bg-border/70 shrink-0" aria-hidden="true" />
)}
<button <button
key={tabId}
type="button" type="button"
onClick={() => onSwitchTab(tabId)} onClick={() => onSwitchTab(tabId)}
className={cn( 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", "group/tab relative flex items-center gap-1.5 px-3 self-stretch text-xs min-w-0 max-w-[220px] transition-colors",
isActive isActive
? "bg-accent text-accent-foreground" ? "bg-background text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground" : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
)} )}
style={{ flex: '1 1 0px' }}
> >
{processing && ( {processing && (
<span className="size-1.5 shrink-0 rounded-full bg-emerald-500 animate-pulse" /> <span className="size-1.5 shrink-0 rounded-full bg-emerald-500 animate-pulse" />
)} )}
<span className="truncate flex-1 text-left">{title}</span> <span className="truncate flex-1 text-left">{title}</span>
{tabs.length > 1 && (
<span <span
role="button" role="button"
className={cn( 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"
"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"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onCloseTab(tabId) onCloseTab(tabId)
@ -69,7 +71,13 @@ export function TabBar<T>({
> >
<X className="size-3" /> <X className="size-3" />
</span> </span>
)}
</button> </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> </div>