会话支持平铺模式显示

This commit is contained in:
xintaofei
2026-03-11 08:14:36 +08:00
parent 2ab6d6ff11
commit 49196ffd4d
14 changed files with 140 additions and 35 deletions

View File

@@ -1,12 +1,21 @@
"use client" "use client"
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import {
memo,
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import { Plus, RefreshCw, X } from "lucide-react" import { Plus, RefreshCw, X } from "lucide-react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { toast } from "sonner" import { toast } from "sonner"
import { disposeTauriListener } from "@/lib/tauri-listener" import { disposeTauriListener } from "@/lib/tauri-listener"
import { useFolderContext } from "@/contexts/folder-context" import { useFolderContext } from "@/contexts/folder-context"
import { useTabContext } from "@/contexts/tab-context" import { useTabContext } from "@/contexts/tab-context"
import { cn } from "@/lib/utils"
import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle" import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle"
import { MessageListView } from "@/components/message/message-list-view" import { MessageListView } from "@/components/message/message-list-view"
import { ConversationShell } from "@/components/chat/conversation-shell" import { ConversationShell } from "@/components/chat/conversation-shell"
@@ -48,6 +57,11 @@ import {
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuTrigger, ContextMenuTrigger,
} from "@/components/ui/context-menu" } from "@/components/ui/context-menu"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
interface ConversationTabViewProps { interface ConversationTabViewProps {
tabId: string tabId: string
@@ -677,8 +691,14 @@ export function ConversationDetailPanel() {
} = useConversationRuntime() } = useConversationRuntime()
const { folder, newConversation, conversations, refreshConversations } = const { folder, newConversation, conversations, refreshConversations } =
useFolderContext() useFolderContext()
const { tabs, activeTabId, openNewConversationTab, closeTab } = const {
useTabContext() tabs,
activeTabId,
isTileMode,
openNewConversationTab,
closeTab,
switchTab,
} = useTabContext()
const [reloadByTabId, setReloadByTabId] = useState<Record<string, number>>({}) const [reloadByTabId, setReloadByTabId] = useState<Record<string, number>>({})
const tabsRef = useRef(tabs) const tabsRef = useRef(tabs)
const conversationsRef = useRef(conversations) const conversationsRef = useRef(conversations)
@@ -859,6 +879,8 @@ export function ConversationDetailPanel() {
openNewConversationTab, openNewConversationTab,
]) ])
const canTile = isTileMode && tabs.length > 1
// Empty state: no tabs at all — show full-screen welcome // Empty state: no tabs at all — show full-screen welcome
if (hasNoTabs) { if (hasNoTabs) {
return null return null
@@ -868,28 +890,65 @@ export function ConversationDetailPanel() {
<ContextMenu> <ContextMenu>
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<div className="relative h-full min-h-0 overflow-hidden"> <div className="relative h-full min-h-0 overflow-hidden">
{tabs.map((tab) => { {canTile ? (
const active = tab.id === activeTabId <ResizablePanelGroup direction="horizontal">
return ( {tabs.map((tab, index) => {
<div const active = tab.id === activeTabId
key={tab.id} return (
className={ <Fragment key={tab.id}>
active {index > 0 && <ResizableHandle withHandle />}
? "h-full" <ResizablePanel
: "absolute inset-0 invisible pointer-events-none" id={`tile-${tab.id}`}
} order={index}
> minSize={15}
<ConversationTabView >
tabId={tab.id} <div
conversationId={tab.conversationId} className={cn(
agentType={tab.agentType} "h-full",
workingDir={tab.workingDir ?? folder?.path} active ? "ring-1 ring-inset ring-primary/30" : ""
isActive={active} )}
reloadSignal={reloadByTabId[tab.id] ?? 0} onPointerDownCapture={() => {
/> if (!active) switchTab(tab.id)
</div> }}
) >
})} <ConversationTabView
tabId={tab.id}
conversationId={tab.conversationId}
agentType={tab.agentType}
workingDir={tab.workingDir ?? folder?.path}
isActive={active}
reloadSignal={reloadByTabId[tab.id] ?? 0}
/>
</div>
</ResizablePanel>
</Fragment>
)
})}
</ResizablePanelGroup>
) : (
tabs.map((tab) => {
const active = tab.id === activeTabId
return (
<div
key={tab.id}
className={
active
? "h-full"
: "absolute inset-0 invisible pointer-events-none"
}
>
<ConversationTabView
tabId={tab.id}
conversationId={tab.conversationId}
agentType={tab.agentType}
workingDir={tab.workingDir ?? folder?.path}
isActive={active}
reloadSignal={reloadByTabId[tab.id] ?? 0}
/>
</div>
)
})
)}
</div> </div>
</ContextMenuTrigger> </ContextMenuTrigger>
<ContextMenuContent> <ContextMenuContent>

View File

@@ -13,11 +13,13 @@ export function TabBar() {
const { const {
tabs, tabs,
activeTabId, activeTabId,
isTileMode,
switchTab, switchTab,
closeTab, closeTab,
closeOtherTabs, closeOtherTabs,
closeAllTabs, closeAllTabs,
pinTab, pinTab,
toggleTileMode,
reorderTabs, reorderTabs,
} = useTabContext() } = useTabContext()
const { mode, activePane } = useWorkspaceContext() const { mode, activePane } = useWorkspaceContext()
@@ -89,11 +91,13 @@ export function TabBar() {
key={tab.id} key={tab.id}
tab={tab} tab={tab}
isActive={tab.id === activeTabId} isActive={tab.id === activeTabId}
isTileMode={isTileMode}
onSwitch={switchTab} onSwitch={switchTab}
onClose={closeTab} onClose={closeTab}
onCloseOthers={closeOtherTabs} onCloseOthers={closeOtherTabs}
onCloseAll={closeAllTabs} onCloseAll={closeAllTabs}
onPin={pinTab} onPin={pinTab}
onToggleTile={toggleTileMode}
/> />
))} ))}
</Reorder.Group> </Reorder.Group>

View File

@@ -19,21 +19,25 @@ import type { TabItem as TabItemData } from "@/contexts/tab-context"
interface TabItemProps { interface TabItemProps {
tab: TabItemData tab: TabItemData
isActive: boolean isActive: boolean
isTileMode: boolean
onSwitch: (tabId: string) => void onSwitch: (tabId: string) => void
onClose: (tabId: string) => void onClose: (tabId: string) => void
onCloseOthers: (tabId: string) => void onCloseOthers: (tabId: string) => void
onCloseAll: () => void onCloseAll: () => void
onPin: (tabId: string) => void onPin: (tabId: string) => void
onToggleTile: () => void
} }
export const TabItem = memo(function TabItem({ export const TabItem = memo(function TabItem({
tab, tab,
isActive, isActive,
isTileMode,
onSwitch, onSwitch,
onClose, onClose,
onCloseOthers, onCloseOthers,
onCloseAll, onCloseAll,
onPin, onPin,
onToggleTile,
}: TabItemProps) { }: TabItemProps) {
const t = useTranslations("Folder.tabs") const t = useTranslations("Folder.tabs")
const isDragging = useRef(false) const isDragging = useRef(false)
@@ -143,6 +147,10 @@ export const TabItem = memo(function TabItem({
{t("closeOthers")} {t("closeOthers")}
</ContextMenuItem> </ContextMenuItem>
<ContextMenuSeparator /> <ContextMenuSeparator />
<ContextMenuItem onSelect={onToggleTile}>
{isTileMode ? t("untileDisplay") : t("tileDisplay")}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onSelect={onCloseAll}> <ContextMenuItem onSelect={onCloseAll}>
{t("closeAll")} {t("closeAll")}
</ContextMenuItem> </ContextMenuItem>

View File

@@ -36,6 +36,7 @@ export type TabItem = TabItemInternal
interface TabContextValue { interface TabContextValue {
tabs: TabItem[] tabs: TabItem[]
activeTabId: string | null activeTabId: string | null
isTileMode: boolean
openTab: ( openTab: (
conversationId: number, conversationId: number,
agentType: AgentType, agentType: AgentType,
@@ -48,6 +49,7 @@ interface TabContextValue {
closeAllTabs: () => void closeAllTabs: () => void
switchTab: (tabId: string) => void switchTab: (tabId: string) => void
pinTab: (tabId: string) => void pinTab: (tabId: string) => void
toggleTileMode: () => void
openNewConversationTab: (agentType: AgentType, workingDir: string) => void openNewConversationTab: (agentType: AgentType, workingDir: string) => void
bindConversationTab: ( bindConversationTab: (
tabId: string, tabId: string,
@@ -394,6 +396,8 @@ export function TabProvider({ children }: TabProviderProps) {
[folder?.path, t] [folder?.path, t]
) )
const [isTileMode, setIsTileMode] = useState(false)
const closeTab = useCallback( const closeTab = useCallback(
(tabId: string) => { (tabId: string) => {
let neighborToSync: TabItemInternal | undefined let neighborToSync: TabItemInternal | undefined
@@ -450,6 +454,7 @@ export function TabProvider({ children }: TabProviderProps) {
const kept = prev.filter((t) => t.id === tabId) const kept = prev.filter((t) => t.id === tabId)
return kept.length === prev.length ? prev : kept return kept.length === prev.length ? prev : kept
}) })
setIsTileMode(false)
const tab = rawTabsRef.current.find((t) => t.id === tabId) const tab = rawTabsRef.current.find((t) => t.id === tabId)
if (tab) { if (tab) {
@@ -470,6 +475,7 @@ export function TabProvider({ children }: TabProviderProps) {
const replacementTab = makeReplacementDraftTab(seedTab) const replacementTab = makeReplacementDraftTab(seedTab)
setTabs([replacementTab]) setTabs([replacementTab])
setIsTileMode(false)
setActiveTabId(replacementTab.id) setActiveTabId(replacementTab.id)
syncFolderContext(replacementTab) syncFolderContext(replacementTab)
activateConversationPane() activateConversationPane()
@@ -493,6 +499,10 @@ export function TabProvider({ children }: TabProviderProps) {
) )
}, []) }, [])
const toggleTileMode = useCallback(() => {
setIsTileMode((prev) => !prev)
}, [])
const reorderTabs = useCallback( const reorderTabs = useCallback(
(reorderedTabs: TabItem[]) => setTabs(reorderedTabs), (reorderedTabs: TabItem[]) => setTabs(reorderedTabs),
[] []
@@ -575,6 +585,7 @@ export function TabProvider({ children }: TabProviderProps) {
() => ({ () => ({
tabs, tabs,
activeTabId, activeTabId,
isTileMode,
openTab, openTab,
closeTab, closeTab,
closeConversationTab, closeConversationTab,
@@ -582,6 +593,7 @@ export function TabProvider({ children }: TabProviderProps) {
closeAllTabs, closeAllTabs,
switchTab, switchTab,
pinTab, pinTab,
toggleTileMode,
openNewConversationTab, openNewConversationTab,
bindConversationTab, bindConversationTab,
reorderTabs, reorderTabs,
@@ -589,6 +601,7 @@ export function TabProvider({ children }: TabProviderProps) {
[ [
tabs, tabs,
activeTabId, activeTabId,
isTileMode,
openTab, openTab,
closeTab, closeTab,
closeConversationTab, closeConversationTab,
@@ -596,6 +609,7 @@ export function TabProvider({ children }: TabProviderProps) {
closeAllTabs, closeAllTabs,
switchTab, switchTab,
pinTab, pinTab,
toggleTileMode,
openNewConversationTab, openNewConversationTab,
bindConversationTab, bindConversationTab,
reorderTabs, reorderTabs,

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "إغلاق تبويب المحادثة", "closeConversationTab": "إغلاق تبويب المحادثة",
"close": "إغلاق", "close": "إغلاق",
"closeOthers": "إغلاق البقية", "closeOthers": "إغلاق البقية",
"closeAll": "إغلاق الكل" "closeAll": "إغلاق الكل",
"tileDisplay": "عرض متجانب",
"untileDisplay": "إلغاء التجانب"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "الملفات", "files": "الملفات",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "Konversationstab schließen", "closeConversationTab": "Konversationstab schließen",
"close": "Schließen", "close": "Schließen",
"closeOthers": "Andere schließen", "closeOthers": "Andere schließen",
"closeAll": "Alle schließen" "closeAll": "Alle schließen",
"tileDisplay": "Kachelansicht",
"untileDisplay": "Kachel beenden"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "Dateien", "files": "Dateien",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "Close conversation tab", "closeConversationTab": "Close conversation tab",
"close": "Close", "close": "Close",
"closeOthers": "Close Others", "closeOthers": "Close Others",
"closeAll": "Close All" "closeAll": "Close All",
"tileDisplay": "Tile Display",
"untileDisplay": "Exit Tile"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "Files", "files": "Files",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "Cerrar pestaña de conversación", "closeConversationTab": "Cerrar pestaña de conversación",
"close": "Cerrar", "close": "Cerrar",
"closeOthers": "Cerrar otros", "closeOthers": "Cerrar otros",
"closeAll": "Cerrar todo" "closeAll": "Cerrar todo",
"tileDisplay": "Vista en mosaico",
"untileDisplay": "Salir de mosaico"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "Archivos", "files": "Archivos",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "Fermer longlet de conversation", "closeConversationTab": "Fermer longlet de conversation",
"close": "Fermer", "close": "Fermer",
"closeOthers": "Fermer les autres", "closeOthers": "Fermer les autres",
"closeAll": "Tout fermer" "closeAll": "Tout fermer",
"tileDisplay": "Affichage en mosaïque",
"untileDisplay": "Quitter la mosaïque"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "Fichiers", "files": "Fichiers",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "会話タブを閉じる", "closeConversationTab": "会話タブを閉じる",
"close": "閉じる", "close": "閉じる",
"closeOthers": "他を閉じる", "closeOthers": "他を閉じる",
"closeAll": "すべて閉じる" "closeAll": "すべて閉じる",
"tileDisplay": "タイル表示",
"untileDisplay": "タイル解除"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "ファイル", "files": "ファイル",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "대화 탭 닫기", "closeConversationTab": "대화 탭 닫기",
"close": "닫기", "close": "닫기",
"closeOthers": "다른 항목 닫기", "closeOthers": "다른 항목 닫기",
"closeAll": "모두 닫기" "closeAll": "모두 닫기",
"tileDisplay": "타일 표시",
"untileDisplay": "타일 해제"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "파일", "files": "파일",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "Fechar aba de conversa", "closeConversationTab": "Fechar aba de conversa",
"close": "Fechar", "close": "Fechar",
"closeOthers": "Fechar outros", "closeOthers": "Fechar outros",
"closeAll": "Fechar tudo" "closeAll": "Fechar tudo",
"tileDisplay": "Exibição em mosaico",
"untileDisplay": "Sair do mosaico"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "Arquivos", "files": "Arquivos",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "关闭会话标签", "closeConversationTab": "关闭会话标签",
"close": "关闭", "close": "关闭",
"closeOthers": "关闭其它", "closeOthers": "关闭其它",
"closeAll": "关闭所有" "closeAll": "关闭所有",
"tileDisplay": "平铺显示",
"untileDisplay": "取消平铺"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "文件", "files": "文件",

View File

@@ -680,7 +680,9 @@
"closeConversationTab": "關閉會話分頁", "closeConversationTab": "關閉會話分頁",
"close": "關閉", "close": "關閉",
"closeOthers": "關閉其它", "closeOthers": "關閉其它",
"closeAll": "關閉所有" "closeAll": "關閉所有",
"tileDisplay": "平鋪顯示",
"untileDisplay": "取消平鋪"
}, },
"fileWorkspace": { "fileWorkspace": {
"files": "檔案", "files": "檔案",