mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-28 19:05:31 +02:00
implement keyboard shortcuts for tab management and adjust layout styles in App and TabBar components
This commit is contained in:
parent
087e809d4c
commit
09288571aa
2 changed files with 99 additions and 47 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue