会话支持平铺模式显示
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -680,7 +680,9 @@
|
||||
"closeConversationTab": "إغلاق تبويب المحادثة",
|
||||
"close": "إغلاق",
|
||||
"closeOthers": "إغلاق البقية",
|
||||
"closeAll": "إغلاق الكل"
|
||||
"closeAll": "إغلاق الكل",
|
||||
"tileDisplay": "عرض متجانب",
|
||||
"untileDisplay": "إلغاء التجانب"
|
||||
},
|
||||
"fileWorkspace": {
|
||||
"files": "الملفات",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -680,7 +680,9 @@
|
||||
"closeConversationTab": "Fermer l’onglet 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",
|
||||
|
||||
@@ -680,7 +680,9 @@
|
||||
"closeConversationTab": "会話タブを閉じる",
|
||||
"close": "閉じる",
|
||||
"closeOthers": "他を閉じる",
|
||||
"closeAll": "すべて閉じる"
|
||||
"closeAll": "すべて閉じる",
|
||||
"tileDisplay": "タイル表示",
|
||||
"untileDisplay": "タイル解除"
|
||||
},
|
||||
"fileWorkspace": {
|
||||
"files": "ファイル",
|
||||
|
||||
@@ -680,7 +680,9 @@
|
||||
"closeConversationTab": "대화 탭 닫기",
|
||||
"close": "닫기",
|
||||
"closeOthers": "다른 항목 닫기",
|
||||
"closeAll": "모두 닫기"
|
||||
"closeAll": "모두 닫기",
|
||||
"tileDisplay": "타일 표시",
|
||||
"untileDisplay": "타일 해제"
|
||||
},
|
||||
"fileWorkspace": {
|
||||
"files": "파일",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -680,7 +680,9 @@
|
||||
"closeConversationTab": "关闭会话标签",
|
||||
"close": "关闭",
|
||||
"closeOthers": "关闭其它",
|
||||
"closeAll": "关闭所有"
|
||||
"closeAll": "关闭所有",
|
||||
"tileDisplay": "平铺显示",
|
||||
"untileDisplay": "取消平铺"
|
||||
},
|
||||
"fileWorkspace": {
|
||||
"files": "文件",
|
||||
|
||||
@@ -680,7 +680,9 @@
|
||||
"closeConversationTab": "關閉會話分頁",
|
||||
"close": "關閉",
|
||||
"closeOthers": "關閉其它",
|
||||
"closeAll": "關閉所有"
|
||||
"closeAll": "關閉所有",
|
||||
"tileDisplay": "平鋪顯示",
|
||||
"untileDisplay": "取消平鋪"
|
||||
},
|
||||
"fileWorkspace": {
|
||||
"files": "檔案",
|
||||
|
||||
Reference in New Issue
Block a user