mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-28 19:05:31 +02:00
added tabs to notes
This commit is contained in:
parent
8773a8ce64
commit
4f3453a65b
3 changed files with 197 additions and 113 deletions
|
|
@ -54,7 +54,7 @@ import { SearchDialog } from '@/components/search-dialog'
|
||||||
import { BackgroundTaskDetail } from '@/components/background-task-detail'
|
import { BackgroundTaskDetail } from '@/components/background-task-detail'
|
||||||
import { FileCardProvider } from '@/contexts/file-card-context'
|
import { FileCardProvider } from '@/contexts/file-card-context'
|
||||||
import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override'
|
import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override'
|
||||||
import { ChatTabBar, type ChatTab } from '@/components/chat-tab-bar'
|
import { TabBar, type ChatTab, type FileTab } from '@/components/tab-bar'
|
||||||
import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js'
|
import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js'
|
||||||
import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js'
|
import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js'
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
@ -663,17 +663,31 @@ function App() {
|
||||||
type RunListItem = { id: string; title?: string; createdAt: string; agentId: string }
|
type RunListItem = { id: string; title?: string; createdAt: string; agentId: string }
|
||||||
const [runs, setRuns] = useState<RunListItem[]>([])
|
const [runs, setRuns] = useState<RunListItem[]>([])
|
||||||
|
|
||||||
// Tab state
|
// Chat tab state
|
||||||
const [openTabs, setOpenTabs] = useState<ChatTab[]>([{ id: 'default-tab', runId: null }])
|
const [chatTabs, setChatTabs] = useState<ChatTab[]>([{ id: 'default-chat-tab', runId: null }])
|
||||||
const [activeTabId, setActiveTabId] = useState('default-tab')
|
const [activeChatTabId, setActiveChatTabId] = useState('default-chat-tab')
|
||||||
const tabIdCounterRef = useRef(0)
|
const chatTabIdCounterRef = useRef(0)
|
||||||
const newTabId = () => `tab-${++tabIdCounterRef.current}`
|
const newChatTabId = () => `chat-tab-${++chatTabIdCounterRef.current}`
|
||||||
|
|
||||||
const getTabTitle = useCallback((tab: ChatTab) => {
|
const getChatTabTitle = useCallback((tab: ChatTab) => {
|
||||||
if (!tab.runId) return 'New chat'
|
if (!tab.runId) return 'New chat'
|
||||||
return runs.find(r => r.id === tab.runId)?.title || '(Untitled chat)'
|
return runs.find(r => r.id === tab.runId)?.title || '(Untitled chat)'
|
||||||
}, [runs])
|
}, [runs])
|
||||||
|
|
||||||
|
const isChatTabProcessing = useCallback((tab: ChatTab) => {
|
||||||
|
return tab.runId ? processingRunIds.has(tab.runId) : false
|
||||||
|
}, [processingRunIds])
|
||||||
|
|
||||||
|
// File tab state
|
||||||
|
const [fileTabs, setFileTabs] = useState<FileTab[]>([])
|
||||||
|
const [activeFileTabId, setActiveFileTabId] = useState<string | null>(null)
|
||||||
|
const fileTabIdCounterRef = useRef(0)
|
||||||
|
const newFileTabId = () => `file-tab-${++fileTabIdCounterRef.current}`
|
||||||
|
|
||||||
|
const getFileTabTitle = useCallback((tab: FileTab) => {
|
||||||
|
return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Pending requests state
|
// Pending requests state
|
||||||
const [pendingPermissionRequests, setPendingPermissionRequests] = useState<Map<string, z.infer<typeof ToolPermissionRequestEvent>>>(new Map())
|
const [pendingPermissionRequests, setPendingPermissionRequests] = useState<Map<string, z.infer<typeof ToolPermissionRequestEvent>>>(new Map())
|
||||||
const [pendingAskHumanRequests, setPendingAskHumanRequests] = useState<Map<string, z.infer<typeof AskHumanRequestEvent>>>(new Map())
|
const [pendingAskHumanRequests, setPendingAskHumanRequests] = useState<Map<string, z.infer<typeof AskHumanRequestEvent>>>(new Map())
|
||||||
|
|
@ -1511,8 +1525,8 @@ function App() {
|
||||||
})
|
})
|
||||||
currentRunId = run.id
|
currentRunId = run.id
|
||||||
setRunId(currentRunId)
|
setRunId(currentRunId)
|
||||||
// Update active tab's runId to the new run
|
// Update active chat tab's runId to the new run
|
||||||
setOpenTabs(prev => prev.map(t => t.id === activeTabId ? { ...t, runId: currentRunId } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: currentRunId } : t))
|
||||||
isNewRun = true
|
isNewRun = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1625,33 +1639,15 @@ function App() {
|
||||||
setAllPermissionRequests(new Map())
|
setAllPermissionRequests(new Map())
|
||||||
setPermissionResponses(new Map())
|
setPermissionResponses(new Map())
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
// Update active tab's runId to null
|
// Update active chat tab's runId to null
|
||||||
setOpenTabs(prev => prev.map(t => t.id === activeTabId ? { ...t, runId: null } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
|
||||||
}, [activeTabId])
|
}, [activeChatTabId])
|
||||||
|
|
||||||
// Tab operations
|
// Chat tab operations
|
||||||
const openInNewTab = useCallback((targetRunId: string) => {
|
const applyChatTab = useCallback((tab: ChatTab) => {
|
||||||
// If already open in a tab, just switch to it
|
|
||||||
const existingTab = openTabs.find(t => t.runId === targetRunId)
|
|
||||||
if (existingTab) {
|
|
||||||
setActiveTabId(existingTab.id)
|
|
||||||
loadRun(targetRunId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const id = newTabId()
|
|
||||||
setOpenTabs(prev => [...prev, { id, runId: targetRunId }])
|
|
||||||
setActiveTabId(id)
|
|
||||||
loadRun(targetRunId)
|
|
||||||
}, [openTabs, loadRun])
|
|
||||||
|
|
||||||
const switchToTab = useCallback((tabId: string) => {
|
|
||||||
const tab = openTabs.find(t => t.id === tabId)
|
|
||||||
if (!tab) return
|
|
||||||
setActiveTabId(tabId)
|
|
||||||
if (tab.runId) {
|
if (tab.runId) {
|
||||||
loadRun(tab.runId)
|
loadRun(tab.runId)
|
||||||
} else {
|
} else {
|
||||||
// Reset to new chat state
|
|
||||||
loadRunRequestIdRef.current += 1
|
loadRunRequestIdRef.current += 1
|
||||||
setConversation([])
|
setConversation([])
|
||||||
setCurrentAssistantMessage('')
|
setCurrentAssistantMessage('')
|
||||||
|
|
@ -1664,50 +1660,93 @@ function App() {
|
||||||
setAllPermissionRequests(new Map())
|
setAllPermissionRequests(new Map())
|
||||||
setPermissionResponses(new Map())
|
setPermissionResponses(new Map())
|
||||||
}
|
}
|
||||||
}, [openTabs, loadRun])
|
}, [loadRun])
|
||||||
|
|
||||||
const closeTab = useCallback((tabId: string) => {
|
const openChatInNewTab = useCallback((targetRunId: string) => {
|
||||||
setOpenTabs(prev => {
|
const existingTab = chatTabs.find(t => t.runId === targetRunId)
|
||||||
|
if (existingTab) {
|
||||||
|
setActiveChatTabId(existingTab.id)
|
||||||
|
loadRun(targetRunId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = newChatTabId()
|
||||||
|
setChatTabs(prev => [...prev, { id, runId: targetRunId }])
|
||||||
|
setActiveChatTabId(id)
|
||||||
|
loadRun(targetRunId)
|
||||||
|
}, [chatTabs, loadRun])
|
||||||
|
|
||||||
|
const switchChatTab = useCallback((tabId: string) => {
|
||||||
|
const tab = chatTabs.find(t => t.id === tabId)
|
||||||
|
if (!tab) return
|
||||||
|
setActiveChatTabId(tabId)
|
||||||
|
applyChatTab(tab)
|
||||||
|
}, [chatTabs, applyChatTab])
|
||||||
|
|
||||||
|
const closeChatTab = useCallback((tabId: string) => {
|
||||||
|
setChatTabs(prev => {
|
||||||
if (prev.length <= 1) return prev
|
if (prev.length <= 1) return prev
|
||||||
const idx = prev.findIndex(t => t.id === tabId)
|
const idx = prev.findIndex(t => t.id === tabId)
|
||||||
const next = prev.filter(t => t.id !== tabId)
|
const next = prev.filter(t => t.id !== tabId)
|
||||||
// If closing the active tab, switch to adjacent
|
if (tabId === activeChatTabId && next.length > 0) {
|
||||||
if (tabId === activeTabId && next.length > 0) {
|
|
||||||
const newIdx = Math.min(idx, next.length - 1)
|
const newIdx = Math.min(idx, next.length - 1)
|
||||||
const newActiveTab = next[newIdx]
|
const newActiveTab = next[newIdx]
|
||||||
setActiveTabId(newActiveTab.id)
|
setActiveChatTabId(newActiveTab.id)
|
||||||
if (newActiveTab.runId) {
|
applyChatTab(newActiveTab)
|
||||||
loadRun(newActiveTab.runId)
|
|
||||||
} else {
|
|
||||||
loadRunRequestIdRef.current += 1
|
|
||||||
setConversation([])
|
|
||||||
setCurrentAssistantMessage('')
|
|
||||||
setRunId(null)
|
|
||||||
setMessage('')
|
|
||||||
setModelUsage(null)
|
|
||||||
setIsProcessing(false)
|
|
||||||
setPendingPermissionRequests(new Map())
|
|
||||||
setPendingAskHumanRequests(new Map())
|
|
||||||
setAllPermissionRequests(new Map())
|
|
||||||
setPermissionResponses(new Map())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
}, [activeTabId, loadRun])
|
}, [activeChatTabId, applyChatTab])
|
||||||
|
|
||||||
|
// File tab operations
|
||||||
|
const openFileInNewTab = useCallback((path: string) => {
|
||||||
|
const existingTab = fileTabs.find(t => t.path === path)
|
||||||
|
if (existingTab) {
|
||||||
|
setActiveFileTabId(existingTab.id)
|
||||||
|
setSelectedPath(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = newFileTabId()
|
||||||
|
setFileTabs(prev => [...prev, { id, path }])
|
||||||
|
setActiveFileTabId(id)
|
||||||
|
setSelectedPath(path)
|
||||||
|
}, [fileTabs])
|
||||||
|
|
||||||
|
const switchFileTab = useCallback((tabId: string) => {
|
||||||
|
const tab = fileTabs.find(t => t.id === tabId)
|
||||||
|
if (!tab) return
|
||||||
|
setActiveFileTabId(tabId)
|
||||||
|
setSelectedPath(tab.path)
|
||||||
|
}, [fileTabs])
|
||||||
|
|
||||||
|
const closeFileTab = useCallback((tabId: string) => {
|
||||||
|
setFileTabs(prev => {
|
||||||
|
if (prev.length <= 1) {
|
||||||
|
// Last file tab - close it and go back to chat
|
||||||
|
setActiveFileTabId(null)
|
||||||
|
setSelectedPath(null)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const idx = prev.findIndex(t => t.id === tabId)
|
||||||
|
const next = prev.filter(t => t.id !== tabId)
|
||||||
|
if (tabId === activeFileTabId && next.length > 0) {
|
||||||
|
const newIdx = Math.min(idx, next.length - 1)
|
||||||
|
const newActiveTab = next[newIdx]
|
||||||
|
setActiveFileTabId(newActiveTab.id)
|
||||||
|
setSelectedPath(newActiveTab.path)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [activeFileTabId])
|
||||||
|
|
||||||
const handleNewChatTab = useCallback(() => {
|
const handleNewChatTab = useCallback(() => {
|
||||||
// If current tab already has a run, open a new tab
|
// If current chat tab already has a run, open a new tab
|
||||||
const activeTab = openTabs.find(t => t.id === activeTabId)
|
const activeTab = chatTabs.find(t => t.id === activeChatTabId)
|
||||||
if (activeTab?.runId) {
|
if (activeTab?.runId) {
|
||||||
const id = newTabId()
|
const id = newChatTabId()
|
||||||
setOpenTabs(prev => [...prev, { id, runId: null }])
|
setChatTabs(prev => [...prev, { id, runId: null }])
|
||||||
setActiveTabId(id)
|
setActiveChatTabId(id)
|
||||||
handleNewChat()
|
|
||||||
} else {
|
|
||||||
// Current tab is already blank, just focus it
|
|
||||||
handleNewChat()
|
|
||||||
}
|
}
|
||||||
|
handleNewChat()
|
||||||
// Ensure we're in chat view
|
// Ensure we're in chat view
|
||||||
if (selectedPath || isGraphOpen || selectedBackgroundTask) {
|
if (selectedPath || isGraphOpen || selectedBackgroundTask) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -1715,7 +1754,7 @@ function App() {
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
}
|
}
|
||||||
}, [openTabs, activeTabId, handleNewChat, selectedPath, isGraphOpen, selectedBackgroundTask])
|
}, [chatTabs, activeChatTabId, handleNewChat, selectedPath, isGraphOpen, selectedBackgroundTask])
|
||||||
|
|
||||||
const handleChatInputSubmit = (text: string) => {
|
const handleChatInputSubmit = (text: string) => {
|
||||||
setIsChatSidebarOpen(true)
|
setIsChatSidebarOpen(true)
|
||||||
|
|
@ -1866,8 +1905,22 @@ function App() {
|
||||||
}, [viewHistory.forward, currentViewState])
|
}, [viewHistory.forward, currentViewState])
|
||||||
|
|
||||||
const navigateToFile = useCallback((path: string) => {
|
const navigateToFile = useCallback((path: string) => {
|
||||||
|
// If already open in a file tab, switch to it
|
||||||
|
const existingTab = fileTabs.find(t => t.path === path)
|
||||||
|
if (existingTab) {
|
||||||
|
switchFileTab(existingTab.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Update current file tab or create one if none exists
|
||||||
|
if (activeFileTabId) {
|
||||||
|
setFileTabs(prev => prev.map(t => t.id === activeFileTabId ? { ...t, path } : t))
|
||||||
|
} else {
|
||||||
|
const id = newFileTabId()
|
||||||
|
setFileTabs(prev => [...prev, { id, path }])
|
||||||
|
setActiveFileTabId(id)
|
||||||
|
}
|
||||||
void navigateToView({ type: 'file', path })
|
void navigateToView({ type: 'file', path })
|
||||||
}, [navigateToView])
|
}, [navigateToView, fileTabs, activeFileTabId, switchFileTab])
|
||||||
|
|
||||||
const navigateToFullScreenChat = useCallback(() => {
|
const navigateToFullScreenChat = useCallback(() => {
|
||||||
// Only treat this as navigation when coming from another view
|
// Only treat this as navigation when coming from another view
|
||||||
|
|
@ -2099,7 +2152,13 @@ function App() {
|
||||||
remove: async (path: string) => {
|
remove: async (path: string) => {
|
||||||
try {
|
try {
|
||||||
await window.ipc.invoke('workspace:remove', { path, opts: { trash: true } })
|
await window.ipc.invoke('workspace:remove', { path, opts: { trash: true } })
|
||||||
if (selectedPath === path) setSelectedPath(null)
|
// Close any file tab showing the deleted file
|
||||||
|
const tabForFile = fileTabs.find(t => t.path === path)
|
||||||
|
if (tabForFile) {
|
||||||
|
closeFileTab(tabForFile.id)
|
||||||
|
} else if (selectedPath === path) {
|
||||||
|
setSelectedPath(null)
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to remove:', err)
|
console.error('Failed to remove:', err)
|
||||||
throw err
|
throw err
|
||||||
|
|
@ -2109,7 +2168,10 @@ function App() {
|
||||||
const fullPath = workspaceRoot ? `${workspaceRoot}/${path}` : path
|
const fullPath = workspaceRoot ? `${workspaceRoot}/${path}` : path
|
||||||
navigator.clipboard.writeText(fullPath)
|
navigator.clipboard.writeText(fullPath)
|
||||||
},
|
},
|
||||||
}), [tree, selectedPath, workspaceRoot, collectDirPaths, navigateToFile, navigateToView])
|
onOpenInNewTab: (path: string) => {
|
||||||
|
openFileInNewTab(path)
|
||||||
|
},
|
||||||
|
}), [tree, selectedPath, workspaceRoot, collectDirPaths, navigateToFile, navigateToView, openFileInNewTab, fileTabs, closeFileTab])
|
||||||
|
|
||||||
// Handler for when a voice note is created/updated
|
// Handler for when a voice note is created/updated
|
||||||
const handleVoiceNoteCreated = useCallback(async (notePath: string) => {
|
const handleVoiceNoteCreated = useCallback(async (notePath: string) => {
|
||||||
|
|
@ -2424,42 +2486,30 @@ function App() {
|
||||||
handleNewChatTab()
|
handleNewChatTab()
|
||||||
},
|
},
|
||||||
onSelectRun: (runIdToLoad) => {
|
onSelectRun: (runIdToLoad) => {
|
||||||
// If already open in a tab, switch to that tab
|
// If already open in a chat tab, switch to it
|
||||||
const existingTab = openTabs.find(t => t.runId === runIdToLoad)
|
const existingTab = chatTabs.find(t => t.runId === runIdToLoad)
|
||||||
if (existingTab) {
|
if (existingTab) {
|
||||||
switchToTab(existingTab.id)
|
switchChatTab(existingTab.id)
|
||||||
if (selectedPath || isGraphOpen || selectedBackgroundTask) {
|
|
||||||
setSelectedPath(null)
|
|
||||||
setIsGraphOpen(false)
|
|
||||||
setExpandedFrom(null)
|
|
||||||
setSelectedBackgroundTask(null)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Navigate current tab to this run
|
// Navigate current chat tab to this run
|
||||||
setOpenTabs(prev => prev.map(t => t.id === activeTabId ? { ...t, runId: runIdToLoad } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
|
||||||
void navigateToView({ type: 'chat', runId: runIdToLoad })
|
void navigateToView({ type: 'chat', runId: runIdToLoad })
|
||||||
},
|
},
|
||||||
onOpenInNewTab: (targetRunId) => {
|
onOpenInNewTab: (targetRunId) => {
|
||||||
openInNewTab(targetRunId)
|
openChatInNewTab(targetRunId)
|
||||||
if (selectedPath || isGraphOpen || selectedBackgroundTask) {
|
|
||||||
setSelectedPath(null)
|
|
||||||
setIsGraphOpen(false)
|
|
||||||
setExpandedFrom(null)
|
|
||||||
setSelectedBackgroundTask(null)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onDeleteRun: async (runIdToDelete) => {
|
onDeleteRun: async (runIdToDelete) => {
|
||||||
try {
|
try {
|
||||||
await window.ipc.invoke('runs:delete', { runId: runIdToDelete })
|
await window.ipc.invoke('runs:delete', { runId: runIdToDelete })
|
||||||
// Close any tab showing the deleted run
|
// Close any chat tab showing the deleted run
|
||||||
const tabForRun = openTabs.find(t => t.runId === runIdToDelete)
|
const tabForRun = chatTabs.find(t => t.runId === runIdToDelete)
|
||||||
if (tabForRun) {
|
if (tabForRun) {
|
||||||
if (openTabs.length > 1) {
|
if (chatTabs.length > 1) {
|
||||||
closeTab(tabForRun.id)
|
closeChatTab(tabForRun.id)
|
||||||
} else {
|
} else {
|
||||||
// Only one tab, reset it to new chat
|
// Only one tab, reset it to new chat
|
||||||
setOpenTabs([{ id: tabForRun.id, runId: null }])
|
setChatTabs([{ id: tabForRun.id, runId: null }])
|
||||||
void navigateToView({ type: 'chat', runId: null })
|
void navigateToView({ type: 'chat', runId: null })
|
||||||
}
|
}
|
||||||
} else if (runId === runIdToDelete) {
|
} else if (runId === runIdToDelete) {
|
||||||
|
|
@ -2486,14 +2536,24 @@ function App() {
|
||||||
canNavigateForward={canNavigateForward}
|
canNavigateForward={canNavigateForward}
|
||||||
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
||||||
>
|
>
|
||||||
{openTabs.length > 1 ? (
|
{selectedPath && fileTabs.length > 1 ? (
|
||||||
<ChatTabBar
|
<TabBar
|
||||||
tabs={openTabs}
|
tabs={fileTabs}
|
||||||
activeTabId={activeTabId}
|
activeTabId={activeFileTabId ?? ''}
|
||||||
getTabTitle={getTabTitle}
|
getTabTitle={getFileTabTitle}
|
||||||
processingRunIds={processingRunIds}
|
getTabId={(t) => t.id}
|
||||||
onSwitchTab={switchToTab}
|
onSwitchTab={switchFileTab}
|
||||||
onCloseTab={closeTab}
|
onCloseTab={closeFileTab}
|
||||||
|
/>
|
||||||
|
) : !selectedPath && !isGraphOpen && !selectedBackgroundTask && chatTabs.length > 1 ? (
|
||||||
|
<TabBar
|
||||||
|
tabs={chatTabs}
|
||||||
|
activeTabId={activeChatTabId}
|
||||||
|
getTabTitle={getChatTabTitle}
|
||||||
|
getTabId={(t) => t.id}
|
||||||
|
isProcessing={isChatTabProcessing}
|
||||||
|
onSwitchTab={switchChatTab}
|
||||||
|
onCloseTab={closeChatTab}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm font-medium text-muted-foreground flex-1 min-w-0 truncate">
|
<span className="text-sm font-medium text-muted-foreground flex-1 min-w-0 truncate">
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ type KnowledgeActions = {
|
||||||
rename: (path: string, newName: string, isDir: boolean) => Promise<void>
|
rename: (path: string, newName: string, isDir: boolean) => Promise<void>
|
||||||
remove: (path: string) => Promise<void>
|
remove: (path: string) => Promise<void>
|
||||||
copyPath: (path: string) => void
|
copyPath: (path: string) => void
|
||||||
|
onOpenInNewTab?: (path: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunListItem = {
|
type RunListItem = {
|
||||||
|
|
@ -1035,12 +1036,27 @@ function Tree({
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem className="group/file-item">
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
isActive={isSelected}
|
isActive={isSelected}
|
||||||
onClick={() => onSelect(item.path, item.kind)}
|
onClick={() => onSelect(item.path, item.kind)}
|
||||||
>
|
>
|
||||||
<span>{item.name}</span>
|
<div className="flex w-full items-center gap-1 min-w-0">
|
||||||
|
<span className="min-w-0 flex-1 truncate">{item.name}</span>
|
||||||
|
{actions.onOpenInNewTab && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shrink-0 hidden group-hover/file-item:flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
actions.onOpenInNewTab!(item.path)
|
||||||
|
}}
|
||||||
|
aria-label="Open in new tab"
|
||||||
|
>
|
||||||
|
<ExternalLink className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
|
|
|
||||||
|
|
@ -7,35 +7,43 @@ export type ChatTab = {
|
||||||
runId: string | null
|
runId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatTabBarProps {
|
export type FileTab = {
|
||||||
tabs: ChatTab[]
|
id: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabBarProps<T> {
|
||||||
|
tabs: T[]
|
||||||
activeTabId: string
|
activeTabId: string
|
||||||
getTabTitle: (tab: ChatTab) => string
|
getTabTitle: (tab: T) => string
|
||||||
processingRunIds: Set<string>
|
getTabId: (tab: T) => string
|
||||||
|
isProcessing?: (tab: T) => boolean
|
||||||
onSwitchTab: (tabId: string) => void
|
onSwitchTab: (tabId: string) => void
|
||||||
onCloseTab: (tabId: string) => void
|
onCloseTab: (tabId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatTabBar({
|
export function TabBar<T>({
|
||||||
tabs,
|
tabs,
|
||||||
activeTabId,
|
activeTabId,
|
||||||
getTabTitle,
|
getTabTitle,
|
||||||
processingRunIds,
|
getTabId,
|
||||||
|
isProcessing,
|
||||||
onSwitchTab,
|
onSwitchTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
}: ChatTabBarProps) {
|
}: 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 items-center gap-0 overflow-x-auto min-w-0">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = tab.id === activeTabId
|
const tabId = getTabId(tab)
|
||||||
const isProcessing = tab.runId ? processingRunIds.has(tab.runId) : false
|
const isActive = tabId === activeTabId
|
||||||
|
const processing = isProcessing?.(tab) ?? false
|
||||||
const title = getTabTitle(tab)
|
const title = getTabTitle(tab)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tabId}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSwitchTab(tab.id)}
|
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 h-full text-xs max-w-[180px] min-w-[80px] transition-colors",
|
||||||
isActive
|
isActive
|
||||||
|
|
@ -43,7 +51,7 @@ export function ChatTabBar({
|
||||||
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
|
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isProcessing && (
|
{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>
|
||||||
|
|
@ -55,7 +63,7 @@ export function ChatTabBar({
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onCloseTab(tab.id)
|
onCloseTab(tabId)
|
||||||
}}
|
}}
|
||||||
aria-label="Close tab"
|
aria-label="Close tab"
|
||||||
>
|
>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue