会话支持平铺模式显示

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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