优化: 工具卡复读修复与默认折叠收敛

This commit is contained in:
lxy
2026-09-29 13:33:49 +08:00
parent 378c363632
commit 1d1111ebe7
2 changed files with 49 additions and 37 deletions
+46 -37
View File
@@ -32,12 +32,12 @@
</div>
</div>
<ToolCard
v-for="(tc, ci) in group.calls"
v-for="tc in group.calls"
:key="tc.id"
:tc="tc"
:isExpanded="isCardExpanded(tc, gi, ci)"
:isExpanded="isCardExpanded(tc, gi)"
:isContentExpanded="expandedTools.has(tc.id)"
@toggle="() => toggleCardExpand(tc, gi, ci)"
@toggle="() => toggleCardExpand(tc, gi)"
@expand-content="toggleExpand"
@approve="(e) => emit('approve', e)"
@install-obscura="() => emit('install-obscura')"
@@ -86,13 +86,16 @@ const expandedCards = ref(new Set<string>())
// userExpandedCards 是"用户明确点开"的持久标记,collapseInactive 跳过这些卡。
// 语义:用户读过的卡不强制折叠,符合"跟随阅读位置"的增强目标。
const userExpandedCards = ref(new Set<string>())
// 用户主动折叠记忆(修复首卡 ci===0 兜底 return true 导致无法折叠)。
// isCardExpanded 最优先判断,覆盖默认首卡展开 + userExpandedCards 记忆。
// 用户主动折叠记忆(与 userExpandedCards 对偶,记录显式收起过的卡)。
// isCardExpanded 最优先判断,覆盖用户展开记忆。
const userCollapsedCards = ref(new Set<string>())
// 内容级展开(read_file "展开全部")
const expandedTools = ref(new Set<string>())
// 分组折叠状态(按分组索引);多卡片分组默认收起
const collapsedGroups = ref<Set<number>>(new Set())
// 用户手动操作过折叠的组(按组名记忆)。流式期间组卡片数增长会反复触发默认收起补齐,
// 用户手动展开过的组不再被自动收回——名字比索引稳(分组只追加不重排,但语义上名字更直白)。
const userTouchedGroups = ref(new Set<string>())
interface ToolCallGroup {
name: string
@@ -146,26 +149,20 @@ const allGroupsExpanded = computed(() => {
)
})
/** 初始化默认折叠:多卡片分组(>=2)默认收起,首条不参与 group-hidden 但跟随折叠态 */
function initDefaultCollapse(): void {
const set = new Set<number>()
for (let i = 0; i < groupedToolCalls.value.length; i++) {
if (groupedToolCalls.value[i].calls.length >= 2) {
set.add(i)
}
}
collapsedGroups.value = set
}
/** 首次有数据时执行默认折叠初始化(watch 防重复,修复异步加载竞态 BUG-B) */
let initialized = false
/**
* 默认收起补齐:达到多卡阈值(>=2)且用户未手动操作过的组自动收起。
* 响应式而非一次性初始化——流式场景第一张卡到达即触发旧一次性逻辑,
* 此后组从 1 卡长到 N 卡不再折叠,批量读取的 N 张单行卡全展开刷屏;
* 签名(组名:卡数)变化即幂等补齐,新达到阈值的组收起,已 touched 的组不动。
*/
watch(
() => props.toolCalls.length,
(len) => {
if (len > 0 && !initialized) {
initialized = true
initDefaultCollapse()
}
() => groupedToolCalls.value.map(g => `${g.name}:${g.calls.length}`).join('|'),
() => {
const next = new Set(collapsedGroups.value)
groupedToolCalls.value.forEach((g, gi) => {
if (g.calls.length >= 2 && !userTouchedGroups.value.has(g.name)) next.add(gi)
})
collapsedGroups.value = next
},
{ immediate: true }
)
@@ -175,6 +172,8 @@ function isGroupCollapsed(gi: number): boolean {
}
function toggleGroupCollapse(gi: number): void {
// 手动操作记 touched:此后该组不被默认收起补齐自动收回
userTouchedGroups.value.add(groupedToolCalls.value[gi]?.name ?? String(gi))
const next = new Set(collapsedGroups.value)
if (next.has(gi)) next.delete(gi)
else next.add(gi)
@@ -184,6 +183,11 @@ function toggleGroupCollapse(gi: number): void {
/** 全部展开 / 全部收起切换 */
function toggleAllGroups(): void {
const next = new Set<number>()
// 全部记 touched:批量操作针对当前可见组,流式新增组仍走默认收起,
// 但已有组不得被补齐逻辑收回(否则"全部展开"点完即被回退)
for (const g of groupedToolCalls.value) {
if (g.calls.length >= 2) userTouchedGroups.value.add(g.name)
}
if (allGroupsExpanded.value) {
// 当前全展开 → 全部收起
for (let i = 0; i < groupedToolCalls.value.length; i++) {
@@ -245,31 +249,31 @@ function groupFileSummary(group: ToolCallGroup): string {
/**
* 单卡是否展开:
* - 分组未折叠 → 用户手动 expandedCards 或首卡默认展开
* - 分组已折叠 → false(整组隐藏)
* - 多卡片分组 + 未手动操作过 → 仅首卡(ci===0)展开
* - 未手动操作过 → 默认折叠(U-260629):completed 卡一律单行 header(工具名+目标+结果摘要),
* 杜绝"AI 一轮多工具各铺开一整卡"刷屏;点 header 才展开。
* running/pending/rejected/write_file 不受此值影响——ToolCard 内部
* v-show="isExpanded || shouldKeepOpen(tc)" 强制展开 body。
* - userExpandedCards(用户主动展开记忆)优先,即使父级 collapseInactive
* 清了 expandedCards,记忆态仍保持展开(用户读过的卡不强制折叠)。
*/
function isCardExpanded(tc: AiToolCallInfo, gi: number, ci: number): boolean {
function isCardExpanded(tc: AiToolCallInfo, gi: number): boolean {
if (isGroupCollapsed(gi)) return false
// 用户主动折叠最优先(覆盖默认首卡 ci===0 兜底展开 + 用户展开记忆)
// 用户主动折叠最优先(覆盖用户展开记忆)
if (userCollapsedCards.value.has(tc.id)) return false
// 用户主动展开的卡片记忆态优先(不被自动收起影响)
if (userExpandedCards.value.has(tc.id)) return true
// 用户手动切换过该卡(当前态)
if (expandedCards.value.has(tc.id)) return true
// 多卡片分组:仅首卡默认展开
if (groupedToolCalls.value[gi]?.calls.length >= 2) return ci === 0
// 单卡组:默认展开
return true
// 兜底:默认折叠(单行 header 已有 toolDisplayName 路径/pattern/host + toolResultSummary 摘要)
return false
}
function toggleCardExpand(tc: AiToolCallInfo, gi: number, ci: number) {
function toggleCardExpand(tc: AiToolCallInfo, gi: number) {
const id = tc.id
// 用 isCardExpanded 判当前真实态(含默认首卡展开),而非 expandedCards(默认首卡不在其中,
// 否则首卡点 toggle 会误判"当前折叠"反向展开)
const currentlyExpanded = isCardExpanded(tc, gi, ci)
// 用 isCardExpanded 判当前真实态(含用户操作记忆),而非 expandedCards(记忆态不在其中,
// 否则已展开卡点 toggle 会误判"当前折叠"反向展开)
const currentlyExpanded = isCardExpanded(tc, gi)
if (currentlyExpanded) {
// 展开 → 折叠
expandedCards.value.delete(id)
@@ -316,7 +320,12 @@ function scrollToFirstPending(): void {
const card = root.querySelector<HTMLElement>('.ai-tool-card--pending_approval')
if (!card) return
if (card.offsetParent === null) {
// 处于折叠分组内 → 展开后下一帧重查
// 处于折叠分组内 → 展开后下一帧重查。
// 展开组记 touched:审批卡可见性优先于默认收起,否则流式新卡一触发补齐,
// 刚展开的含 pending 卡的组又被收回,BUG-260624-02(审批不可见)复发。
groupedToolCalls.value.forEach(g => {
if (g.calls.length >= 2) userTouchedGroups.value.add(g.name)
})
collapsedGroups.value = new Set()
nextTick(() => {
root.querySelector<HTMLElement>('.ai-tool-card--pending_approval')