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

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