From 1d1111ebe722a5c0a72f767609486487bfd37ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 29 Sep 2026 13:33:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8D=A1=E5=A4=8D=E8=AF=BB=E4=BF=AE=E5=A4=8D=E4=B8=8E=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=8A=98=E5=8F=A0=E6=94=B6=E6=95=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ai.ts | 3 ++ src/components/ToolCardList.vue | 83 ++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 37 deletions(-) diff --git a/src/api/ai.ts b/src/api/ai.ts index 5520826..3acf60f 100644 --- a/src/api/ai.ts +++ b/src/api/ai.ts @@ -162,6 +162,8 @@ export const aiApi = { apiKey: string defaultModel: string modelConfigs: ModelConfig[] + /** 供应商级采样温度覆盖(写 config JSON);缺省不传 → 后端 config 保持 None,主链路走默认 0.7 */ + temperatureOverride?: number }): Promise { return invoke('ai_save_provider', { id: input.id, @@ -171,6 +173,7 @@ export const aiApi = { apiKey: input.apiKey, defaultModel: input.defaultModel, modelConfigs: input.modelConfigs, + temperatureOverride: input.temperatureOverride, }) }, diff --git a/src/components/ToolCardList.vue b/src/components/ToolCardList.vue index fff7db7..1d0e592 100644 --- a/src/components/ToolCardList.vue +++ b/src/components/ToolCardList.vue @@ -32,12 +32,12 @@ ()) // userExpandedCards 是"用户明确点开"的持久标记,collapseInactive 跳过这些卡。 // 语义:用户读过的卡不强制折叠,符合"跟随阅读位置"的增强目标。 const userExpandedCards = ref(new Set()) -// 用户主动折叠记忆(修复首卡 ci===0 兜底 return true 导致无法折叠)。 -// isCardExpanded 最优先判断,覆盖默认首卡展开 + userExpandedCards 记忆。 +// 用户主动折叠记忆(与 userExpandedCards 对偶,记录显式收起过的卡)。 +// isCardExpanded 最优先判断,覆盖用户展开记忆。 const userCollapsedCards = ref(new Set()) // 内容级展开(read_file "展开全部") const expandedTools = ref(new Set()) // 分组折叠状态(按分组索引);多卡片分组默认收起 const collapsedGroups = ref>(new Set()) +// 用户手动操作过折叠的组(按组名记忆)。流式期间组卡片数增长会反复触发默认收起补齐, +// 用户手动展开过的组不再被自动收回——名字比索引稳(分组只追加不重排,但语义上名字更直白)。 +const userTouchedGroups = ref(new Set()) interface ToolCallGroup { name: string @@ -146,26 +149,20 @@ const allGroupsExpanded = computed(() => { ) }) -/** 初始化默认折叠:多卡片分组(>=2)默认收起,首条不参与 group-hidden 但跟随折叠态 */ -function initDefaultCollapse(): void { - const set = new Set() - 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() + // 全部记 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('.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('.ai-tool-card--pending_approval')