feat(web): 重构智能体文件侧边栏

This commit is contained in:
Wenjie Zhang 2026-06-10 14:08:41 +08:00
parent 84dc55ee74
commit a8536a2e85
3 changed files with 254 additions and 100 deletions

View File

@ -1,6 +1,13 @@
<template> <template>
<div class="chat-container"> <div class="chat-container">
<div class="chat"> <div
class="chat"
:class="{
'has-file-panel': isFilePanelOpen,
'is-resizing-file-panel': isResizing
}"
:style="{ '--file-panel-width': filePanelWidthStyle }"
>
<div class="chat-header"> <div class="chat-header">
<div class="header__left"> <div class="header__left">
<slot name="header-left"></slot> <slot name="header-left"></slot>
@ -16,16 +23,32 @@
v-if="showStateEntry" v-if="showStateEntry"
type="button" type="button"
class="agent-nav-btn agent-state-btn state-entry-btn" class="agent-nav-btn agent-state-btn state-entry-btn"
:class="{ active: sideActive === 'state' }" :class="{ active: statePanelOpen }"
title="查看状态" title="查看状态"
:aria-expanded="statePanelOpen"
aria-controls="agent-state-panel"
@click.stop="toggleStatePanel" @click.stop="toggleStatePanel"
> >
<SquareCheck size="18" class="nav-btn-icon" /> <SquareCheck size="18" class="nav-btn-icon" />
<span class="hide-text">状态</span> <span class="hide-text">状态</span>
</button> </button>
<button
v-if="showFileEntry && !isFilePanelOpen"
type="button"
class="agent-nav-btn agent-state-btn file-entry-btn"
title="查看文件"
:aria-expanded="isFilePanelOpen"
aria-controls="agent-file-panel"
@click.stop="toggleAgentPanel"
>
<FolderKanban size="18" class="nav-btn-icon" />
<span class="hide-text">文件</span>
</button>
<slot <slot
name="header-right" name="header-right"
:side-active="sideActive" :side-active="sideActive"
:is-file-panel-open="isFilePanelOpen"
:is-state-panel-open="statePanelOpen"
:has-active-thread="!!currentChatId" :has-active-thread="!!currentChatId"
:toggle-agent-panel="toggleAgentPanel" :toggle-agent-panel="toggleAgentPanel"
></slot> ></slot>
@ -36,8 +59,9 @@
ref="chatContentContainerRef" ref="chatContentContainerRef"
class="chat-content-container" class="chat-content-container"
:class="{ :class="{
'has-file-panel': sideActive === 'file', 'has-file-panel': isFilePanelOpen,
'has-state-panel': sideActive === 'state' 'has-state-panel': statePanelDocked,
'has-floating-state-panel': statePanelFloating
}" }"
> >
<!-- Main Chat Area --> <!-- Main Chat Area -->
@ -163,46 +187,19 @@
</div> </div>
</div> </div>
<!-- Agent Panel Area -->
<div
class="side-panel side-panel--file"
ref="panelWrapperRef"
:class="{
'is-visible': sideActive === 'file',
'no-transition': isResizing
}"
:style="{
flexBasis: sideActive === 'file' ? `${panelRatio * 100}%` : '0px'
}"
>
<AgentPanel
v-if="sideActive === 'file'"
:agent-state="currentAgentState"
:thread-id="currentChatId"
:panel-ratio="panelRatio"
:preview-tabs="agentPanelPreviewTabs"
:active-preview-path="agentPanelActivePreviewPath"
:view-mode="agentPanelViewMode"
@refresh="handleAgentStateRefresh"
@resize="handlePanelResize"
@resizing="handleResizingChange"
@open-preview="openPanelPreview"
@activate-preview="activatePanelPreview"
@close-preview-tab="closePanelPreviewTab"
@close-preview-path="closePanelPreviewPath"
@view-mode-change="setAgentPanelViewMode"
/>
</div>
<div <div
id="agent-state-panel"
class="side-panel side-panel--state" class="side-panel side-panel--state"
:class="{ 'is-visible': sideActive === 'state' }" :class="{
'is-visible': statePanelOpen,
'is-docked': statePanelDocked,
'is-floating': statePanelFloating
}"
:style="{ :style="{
flexBasis: sideActive === 'state' ? '340px' : '0px' flexBasis: statePanelDocked ? `${statePanelDockWidth}px` : '0px'
}" }"
> >
<div v-if="sideActive === 'state'" class="state-panel"> <div v-if="statePanelOpen" class="state-panel">
<div class="side-panel__header state-panel-header"> <div class="side-panel__header state-panel-header">
<span class="state-panel-title">状态</span> <span class="state-panel-title">状态</span>
<div class="state-panel-header-actions"> <div class="state-panel-header-actions">
@ -442,6 +439,38 @@
</div> </div>
</div> </div>
</div> </div>
<div
id="agent-file-panel"
class="side-panel side-panel--file"
ref="panelWrapperRef"
:class="{
'is-visible': isFilePanelOpen,
'no-transition': isResizing
}"
:style="{
width: filePanelWidthStyle
}"
>
<AgentPanel
v-if="isFilePanelOpen"
:agent-state="currentAgentState"
:thread-id="currentChatId"
:panel-ratio="panelRatio"
:preview-tabs="agentPanelPreviewTabs"
:active-preview-path="agentPanelActivePreviewPath"
:view-mode="agentPanelViewMode"
@close="closeFilePanel"
@refresh="handleAgentStateRefresh"
@resize="handlePanelResize"
@resizing="handleResizingChange"
@open-preview="openPanelPreview"
@activate-preview="activatePanelPreview"
@close-preview-tab="closePanelPreviewTab"
@close-preview-path="closePanelPreviewPath"
@view-mode-change="setAgentPanelViewMode"
/>
</div>
</div> </div>
<SubagentThreadModal <SubagentThreadModal
@ -468,7 +497,7 @@ import {
onDeactivated onDeactivated
} from 'vue' } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ChevronDown, RefreshCw, SquareCheck } from 'lucide-vue-next' import { ChevronDown, FolderKanban, RefreshCw, SquareCheck } from 'lucide-vue-next'
import { formatFileSize } from '@/utils/file_utils' import { formatFileSize } from '@/utils/file_utils'
import FileTypeIcon from '@/components/common/FileTypeIcon.vue' import FileTypeIcon from '@/components/common/FileTypeIcon.vue'
import { generatePixelAvatar } from '@/utils/pixelAvatar' import { generatePixelAvatar } from '@/utils/pixelAvatar'
@ -593,18 +622,30 @@ const configNoticeScrollVersion = ref(0)
// UI 使 // UI 使
const localUIState = reactive({ const localUIState = reactive({
chatMainWidth: typeof window !== 'undefined' ? window.innerWidth : 0 chatMainWidth: typeof window !== 'undefined' ? window.innerWidth : 0,
chatContentWidth: typeof window !== 'undefined' ? window.innerWidth : 0
}) })
// Agent Panel State // Agent Panel State
const sideActive = ref('') const isFilePanelOpen = ref(false)
const statePanelOpen = ref(false)
const sideActive = computed(() => {
if (isFilePanelOpen.value) return 'file'
if (statePanelOpen.value) return 'state'
return ''
})
const isResizing = ref(false) const isResizing = ref(false)
const defaultPanelRatio = 0.3 const defaultPanelRatio = 0.3
const previewPanelRatio = 0.65 const previewPanelRatio = 0.65
const minPanelRatio = 0.25 const minPanelRatio = 0.25
const maxPanelRatio = 0.75 const maxPanelRatio = 0.75
const minChatMainWidth = 350 const minChatMainWidth = 350
const filePanelGapWidth = 0
const mobilePanelBreakpoint = 768
const statePanelDockWidth = 340
const statePanelDockMinChatWidth = 800
const panelRatio = ref(defaultPanelRatio) // (0-1) const panelRatio = ref(defaultPanelRatio) // (0-1)
const filePanelDragWidth = ref(null)
const agentPanelPreviewTabs = ref([]) const agentPanelPreviewTabs = ref([])
const agentPanelActivePreviewPath = ref('') const agentPanelActivePreviewPath = ref('')
const agentPanelViewMode = ref('tree') const agentPanelViewMode = ref('tree')
@ -627,11 +668,22 @@ const getPanelContainerWidth = () => {
return container?.clientWidth || (typeof window !== 'undefined' ? window.innerWidth : 0) return container?.clientWidth || (typeof window !== 'undefined' ? window.innerWidth : 0)
} }
const getFilePanelMaxWidth = (containerWidth = getPanelContainerWidth()) => {
if (!containerWidth) return 0
if (containerWidth <= mobilePanelBreakpoint) return Math.max(0, containerWidth - 16)
return Math.max(0, containerWidth - minChatMainWidth - filePanelGapWidth)
}
const getFilePanelMinWidth = (containerWidth, maxWidth = getFilePanelMaxWidth(containerWidth)) => {
const preferredMinWidth = containerWidth <= mobilePanelBreakpoint ? 280 : 320
return Math.min(preferredMinWidth, maxWidth)
}
const getMaxPanelRatio = (containerWidth = getPanelContainerWidth()) => { const getMaxPanelRatio = (containerWidth = getPanelContainerWidth()) => {
if (!containerWidth) return maxPanelRatio if (!containerWidth) return maxPanelRatio
return Math.max( return Math.max(
minPanelRatio, minPanelRatio,
Math.min(maxPanelRatio, (containerWidth - minChatMainWidth) / containerWidth) Math.min(maxPanelRatio, getFilePanelMaxWidth(containerWidth) / containerWidth)
) )
} }
@ -639,20 +691,43 @@ const clampPanelRatio = (ratio, containerWidth = getPanelContainerWidth()) => {
return Math.max(minPanelRatio, Math.min(ratio, getMaxPanelRatio(containerWidth))) return Math.max(minPanelRatio, Math.min(ratio, getMaxPanelRatio(containerWidth)))
} }
const filePanelWidthStyle = computed(() => {
if (!isFilePanelOpen.value) return '0px'
if (filePanelDragWidth.value !== null) return `${filePanelDragWidth.value}px`
const containerWidth = localUIState.chatContentWidth || getPanelContainerWidth()
if (!containerWidth) return `${panelRatio.value * 100}%`
const maxWidth = getFilePanelMaxWidth(containerWidth)
const minWidth = getFilePanelMinWidth(containerWidth, maxWidth)
const preferredWidth = containerWidth * panelRatio.value
return `${Math.max(minWidth, Math.min(preferredWidth, maxWidth))}px`
})
const statePanelCanDock = computed(() => {
if (isFilePanelOpen.value) return false
const containerWidth = localUIState.chatContentWidth || getPanelContainerWidth()
return containerWidth - statePanelDockWidth > statePanelDockMinChatWidth
})
const statePanelDocked = computed(() => statePanelOpen.value && statePanelCanDock.value)
const statePanelFloating = computed(() => statePanelOpen.value && !statePanelDocked.value)
const setPanelRatioForViewMode = () => { const setPanelRatioForViewMode = () => {
const hasPreview = Boolean(agentPanelActivePreviewPath.value) const hasPreview = Boolean(agentPanelActivePreviewPath.value)
panelRatio.value = clampPanelRatio(hasPreview ? previewPanelRatio : defaultPanelRatio) panelRatio.value = clampPanelRatio(hasPreview ? previewPanelRatio : defaultPanelRatio)
} }
const showFilePanel = (mode = 'tree') => { const showFilePanel = (mode = 'tree') => {
sideActive.value = 'file' isFilePanelOpen.value = true
statePanelOpen.value = false
agentPanelViewMode.value = agentPanelViewMode.value =
mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree' mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
setPanelRatioForViewMode() setPanelRatioForViewMode()
} }
const showFileTreePanel = () => { const showFileTreePanel = () => {
sideActive.value = 'file' isFilePanelOpen.value = true
statePanelOpen.value = false
agentPanelActivePreviewPath.value = '' agentPanelActivePreviewPath.value = ''
agentPanelViewMode.value = 'tree' agentPanelViewMode.value = 'tree'
setPanelRatioForViewMode() setPanelRatioForViewMode()
@ -709,7 +784,8 @@ const isSameOrChildPanelPath = (path, targetPath) => {
} }
const resetAgentPanelState = () => { const resetAgentPanelState = () => {
sideActive.value = '' isFilePanelOpen.value = false
statePanelOpen.value = false
panelRatio.value = defaultPanelRatio panelRatio.value = defaultPanelRatio
agentPanelPreviewTabs.value = [] agentPanelPreviewTabs.value = []
agentPanelActivePreviewPath.value = '' agentPanelActivePreviewPath.value = ''
@ -957,6 +1033,7 @@ const completedTodoCount = computed(
() => currentTodos.value.filter((todo) => todo?.status === 'completed').length () => currentTodos.value.filter((todo) => todo?.status === 'completed').length
) )
const showStateEntry = computed(() => Boolean(currentChatId.value)) const showStateEntry = computed(() => Boolean(currentChatId.value))
const showFileEntry = computed(() => Boolean(currentChatId.value))
const todoProgress = computed(() => { const todoProgress = computed(() => {
if (!totalTodoCount.value) return 0 if (!totalTodoCount.value) return 0
return Math.round((completedTodoCount.value / totalTodoCount.value) * 100) return Math.round((completedTodoCount.value / totalTodoCount.value) * 100)
@ -1256,7 +1333,7 @@ const isStreaming = computed(() => {
return threadState ? threadState.isStreaming : false return threadState ? threadState.isStreaming : false
}) })
const shouldRefreshStateWhileStreaming = computed( const shouldRefreshStateWhileStreaming = computed(
() => Boolean(currentChatId.value) && isStreaming.value && sideActive.value === 'state' () => Boolean(currentChatId.value) && isStreaming.value && statePanelOpen.value
) )
const isProcessing = computed(() => isStreaming.value) const isProcessing = computed(() => isStreaming.value)
const isReplyLoading = computed(() => { const isReplyLoading = computed(() => {
@ -1562,19 +1639,24 @@ const startChatMainResizeObserver = () => {
return return
} }
localUIState.chatMainWidth = chatMainRef.value.clientWidth || window.innerWidth const syncLayoutWidths = () => {
localUIState.chatMainWidth = chatMainRef.value?.clientWidth || window.innerWidth
localUIState.chatContentWidth =
chatContentContainerRef.value?.clientWidth || localUIState.chatMainWidth
}
syncLayoutWidths()
chatMainResizeObserver = new ResizeObserver((entries) => { chatMainResizeObserver = new ResizeObserver((entries) => {
// layout // layout
if (!isResizeObserverReady) return if (!isResizeObserverReady) return
for (const entry of entries) { if (!entries.length) return
const width = entry.contentRect.width syncLayoutWidths()
if (!width) continue
localUIState.chatMainWidth = width
}
}) })
chatMainResizeObserver.observe(chatMainRef.value) chatMainResizeObserver.observe(chatMainRef.value)
if (chatContentContainerRef.value) {
chatMainResizeObserver.observe(chatContentContainerRef.value)
}
armResizeObserver() armResizeObserver()
} }
@ -2203,18 +2285,23 @@ const handleAgentStateRefresh = async (threadId = null) => {
} }
const toggleStatePanel = async () => { const toggleStatePanel = async () => {
const nextOpen = sideActive.value !== 'state' const nextOpen = !statePanelOpen.value
sideActive.value = nextOpen ? 'state' : '' statePanelOpen.value = nextOpen
if (nextOpen && currentChatId.value && !currentAgentState.value) { if (nextOpen && currentChatId.value && !currentAgentState.value) {
await handleAgentStateRefresh() await handleAgentStateRefresh()
} }
} }
const closeFilePanel = () => {
isFilePanelOpen.value = false
filePanelDragWidth.value = null
}
const toggleAgentPanel = async () => { const toggleAgentPanel = async () => {
const nextOpen = sideActive.value !== 'file' const nextOpen = !isFilePanelOpen.value
if (!nextOpen) { if (!nextOpen) {
sideActive.value = '' closeFilePanel()
return return
} }
@ -2233,11 +2320,11 @@ const handlePanelResize = (clientX) => {
const deltaX = clientX - resizeStartX const deltaX = clientX - resizeStartX
const rawWidth = resizeStartWidth - deltaX const rawWidth = resizeStartWidth - deltaX
const minWidth = minPanelRatio * panelContainerWidth const maxWidth = getFilePanelMaxWidth(panelContainerWidth)
const maxWidth = getMaxPanelRatio(panelContainerWidth) * panelContainerWidth const minWidth = getFilePanelMinWidth(panelContainerWidth, maxWidth)
const nextWidth = Math.max(minWidth, Math.min(rawWidth, maxWidth)) const nextWidth = Math.max(minWidth, Math.min(rawWidth, maxWidth))
panelWrapperRef.value.style.setProperty('flex', `0 0 ${nextWidth}px`, 'important') filePanelDragWidth.value = nextWidth
if (nextWidth !== rawWidth) { if (nextWidth !== rawWidth) {
resizeStartX = clientX resizeStartX = clientX
@ -2252,6 +2339,7 @@ const handleResizingChange = (isResizingState, clientX = 0) => {
if (isResizingState && panelWrapperRef.value) { if (isResizingState && panelWrapperRef.value) {
resizeStartX = clientX resizeStartX = clientX
resizeStartWidth = panelWrapperRef.value.offsetWidth resizeStartWidth = panelWrapperRef.value.offsetWidth
filePanelDragWidth.value = resizeStartWidth
if (!panelContainerWidth) { if (!panelContainerWidth) {
panelContainerWidth = getPanelContainerWidth() panelContainerWidth = getPanelContainerWidth()
} }
@ -2259,9 +2347,12 @@ const handleResizingChange = (isResizingState, clientX = 0) => {
} }
if (!isResizingState && panelWrapperRef.value && panelContainerWidth) { if (!isResizingState && panelWrapperRef.value && panelContainerWidth) {
const finalWidth = panelWrapperRef.value.offsetWidth const finalWidth = filePanelDragWidth.value ?? panelWrapperRef.value.offsetWidth
panelRatio.value = clampPanelRatio(finalWidth / panelContainerWidth, panelContainerWidth) panelRatio.value = clampPanelRatio(finalWidth / panelContainerWidth, panelContainerWidth)
panelWrapperRef.value.style.removeProperty('flex') }
if (!isResizingState) {
filePanelDragWidth.value = null
resizeStartX = 0 resizeStartX = 0
resizeStartWidth = 0 resizeStartWidth = 0
panelContainerWidth = 0 // 使 panelContainerWidth = 0 // 使
@ -2374,8 +2465,14 @@ onMounted(async () => {
}) })
watch(showStateEntry, (visible) => { watch(showStateEntry, (visible) => {
if (!visible && sideActive.value === 'state') { if (!visible && statePanelOpen.value) {
sideActive.value = '' statePanelOpen.value = false
}
})
watch(showFileEntry, (visible) => {
if (!visible && isFilePanelOpen.value) {
closeFilePanel()
} }
}) })
@ -2507,6 +2604,7 @@ watch(currentChatId, (threadId, oldThreadId) => {
align-items: center; align-items: center;
padding: 1rem 8px; padding: 1rem 8px;
flex-shrink: 0; /* Prevent header from shrinking */ flex-shrink: 0; /* Prevent header from shrinking */
transition: padding-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.header__left, .header__left,
.header__right { .header__right {
@ -2535,6 +2633,17 @@ watch(currentChatId, (threadId, oldThreadId) => {
margin-left: 8px; margin-left: 8px;
} }
} }
&.has-file-panel .chat-header {
padding-right: calc(var(--file-panel-width) + 8px);
}
&.is-resizing-file-panel {
.chat-header,
.chat-main {
transition: none;
}
}
} }
.chat-content-container { .chat-content-container {
@ -2555,39 +2664,43 @@ watch(currentChatId, (threadId, oldThreadId) => {
position: relative; position: relative;
transition: transition:
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1), flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1),
width 0.3s cubic-bezier(0.4, 0, 0.2, 1); width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
min-width: 0; /* Prevent flex item from overflowing */ min-width: 0; /* Prevent flex item from overflowing */
scrollbar-width: none; scrollbar-width: none;
} }
.chat-content-container.has-file-panel .chat-main {
margin-right: var(--file-panel-width);
}
.side-panel { .side-panel {
flex: 0 0 auto; flex: 0 0 auto;
overflow: hidden; overflow: hidden;
background: var(--gray-0); background: var(--gray-0);
border: 1px solid var(--gray-150); border: 1px solid var(--gray-150);
border-radius: 16px; border-radius: 10px;
box-shadow: box-shadow:
0 16px 40px var(--shadow-1), 0 16px 40px var(--shadow-1),
0 2px 10px var(--shadow-0); 0 2px 10px var(--shadow-0);
z-index: 20; z-index: 20;
margin: 0 8px 8px;
margin-left: -16px;
min-width: 0; min-width: 0;
opacity: 0; opacity: 0;
pointer-events: none;
transform: translateX(10px); transform: translateX(10px);
will-change: flex-basis; will-change: width, flex-basis, opacity, transform;
transition: transition:
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1), flex-basis 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
} }
.side-panel.is-visible { .side-panel.is-visible {
opacity: 1; opacity: 1;
pointer-events: auto;
transform: translateX(0); transform: translateX(0);
margin-left: 0;
} }
.side-panel.no-transition { .side-panel.no-transition {
@ -2595,16 +2708,25 @@ watch(currentChatId, (threadId, oldThreadId) => {
} }
.side-panel--file { .side-panel--file {
align-self: stretch; position: absolute;
top: 0;
right: 0;
bottom: 0;
z-index: 30;
display: flex;
height: auto; height: auto;
max-width: 100%;
border: none;
border-left: 1px solid var(--gray-150);
border-radius: 0;
box-shadow: none;
} }
.side-panel--file.is-visible { .side-panel--file.is-visible {
min-width: 320px; min-width: 0;
} }
.side-panel--state { .side-panel--state {
align-self: flex-start;
height: auto; height: auto;
max-height: calc(100% - 8px); max-height: calc(100% - 8px);
max-width: min(340px, calc(100vw - 24px)); max-width: min(340px, calc(100vw - 24px));
@ -2616,6 +2738,25 @@ watch(currentChatId, (threadId, oldThreadId) => {
min-width: 300px; min-width: 300px;
} }
.side-panel--state.is-docked {
align-self: flex-start;
margin: 0 8px 8px 0;
}
.side-panel--state.is-floating {
position: absolute;
top: 8px;
right: calc(var(--file-panel-width) + 8px);
width: min(340px, calc(100% - var(--file-panel-width) - 24px));
min-width: 0;
max-height: calc(100% - 16px);
margin: 0;
z-index: 26;
box-shadow:
0 12px 28px var(--shadow-1),
0 2px 8px var(--shadow-0);
}
.state-panel { .state-panel {
height: 100%; height: 100%;
display: flex; display: flex;
@ -2779,7 +2920,6 @@ watch(currentChatId, (threadId, oldThreadId) => {
margin: 0 auto; margin: 0 auto;
flex-grow: 1; flex-grow: 1;
padding: 1rem var(--page-padding); padding: 1rem var(--page-padding);
padding-right: 30px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -2931,25 +3071,39 @@ watch(currentChatId, (threadId, oldThreadId) => {
} }
.side-panel--file.is-visible, .side-panel--file.is-visible,
.side-panel--state.is-visible { .side-panel--state.is-docked.is-visible {
max-width: calc(100% - 350px); max-width: 100%;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.chat.has-file-panel .chat-header {
padding-right: 8px;
}
.chat-content-container.has-file-panel .chat-main, .chat-content-container.has-file-panel .chat-main,
.chat-content-container.has-state-panel .chat-main { .chat-content-container.has-state-panel .chat-main {
margin-right: 0;
min-width: 0; min-width: 0;
} }
.side-panel--file {
top: calc(var(--header-height) + 4px);
}
.side-panel--file.is-visible { .side-panel--file.is-visible {
min-width: 280px; min-width: 0;
max-width: 80%; max-width: calc(100% - 16px);
} }
.side-panel--state.is-visible { .side-panel--state.is-visible {
min-width: 260px; min-width: 0;
max-width: 80%; max-width: calc(100% - 24px);
}
.side-panel--state.is-floating {
right: 12px;
width: min(320px, calc(100% - 24px));
} }
.agent-segment-wrapper { .agent-segment-wrapper {

View File

@ -46,6 +46,14 @@
<button class="header-action-btn" title="刷新" aria-label="刷新" @click="emitRefresh"> <button class="header-action-btn" title="刷新" aria-label="刷新" @click="emitRefresh">
<RefreshCw :size="15" /> <RefreshCw :size="15" />
</button> </button>
<button
class="header-action-btn"
title="关闭文件面板"
aria-label="关闭文件面板"
@click="emitClose"
>
<PanelRightClose :size="15" />
</button>
</div> </div>
</div> </div>
@ -129,7 +137,7 @@
<script setup> <script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { Download, Folders, RefreshCw, Trash2, X } from 'lucide-vue-next' import { Download, Folders, PanelRightClose, RefreshCw, Trash2, X } from 'lucide-vue-next'
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import FileTreeComponent from '@/components/FileTreeComponent.vue' import FileTreeComponent from '@/components/FileTreeComponent.vue'
import AgentFilePreview from '@/components/AgentFilePreview.vue' import AgentFilePreview from '@/components/AgentFilePreview.vue'
@ -170,6 +178,7 @@ const props = defineProps({
}) })
const emit = defineEmits([ const emit = defineEmits([
'close',
'refresh', 'refresh',
'resize', 'resize',
'resizing', 'resizing',
@ -541,6 +550,10 @@ const emitRefresh = () => {
emit('refresh', props.threadId) emit('refresh', props.threadId)
} }
const emitClose = () => {
emit('close')
}
let resizePointerId = null let resizePointerId = null
let pendingClientX = 0 let pendingClientX = 0
let resizeFrameId = 0 let resizeFrameId = 0
@ -663,7 +676,9 @@ watch(
} }
.agent-panel { .agent-panel {
width: 100%;
height: 100%; height: 100%;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
@ -675,8 +690,7 @@ watch(
} }
.panel-header { .panel-header {
border-bottom: none; border-bottom: 1px solid var(--gray-100);
padding-bottom: 0;
} }
:deep(.side-preview-shell) { :deep(.side-preview-shell) {

View File

@ -78,20 +78,6 @@
</template> </template>
</a-dropdown> </a-dropdown>
</template> </template>
<template #header-right="{ sideActive, hasActiveThread, toggleAgentPanel }">
<button
v-if="hasActiveThread"
type="button"
class="agent-nav-btn agent-state-btn"
:class="{ active: sideActive === 'file' }"
title="查看文件"
@click.stop="toggleAgentPanel"
>
<FolderKanban size="18" class="nav-btn-icon" />
<span class="hide-text">文件</span>
</button>
</template>
</AgentChatComponent> </AgentChatComponent>
</div> </div>
</div> </div>
@ -101,7 +87,7 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { Settings2, ChevronDown, Check, FolderKanban } from 'lucide-vue-next' import { Settings2, ChevronDown, Check } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import AgentChatComponent from '@/components/AgentChatComponent.vue' import AgentChatComponent from '@/components/AgentChatComponent.vue'
import { isBuiltinAgent, useAgentStore } from '@/stores/agent' import { isBuiltinAgent, useAgentStore } from '@/stores/agent'