优化: AI Chat 渲染与无障碍 + 文件查看 + 同步 IO 收口

- 渲染: 结果/过程分离(气泡只放最终结果,过程轮叙述+工具卡收进轨迹卡默认收起可展开——治『流式后正文消失』且不再 N 气泡打扰)
- 渲染 P0: v-memo 定向 dep(只有真正渲染流式块的 item 失效,其余取占位) + renderItems 桶 memo(签名覆盖全消费输入,item 引用稳定) + 指纹 watch 降为 O(1) toolEpoch
- TopBar 三处 O(n) messages computed 合并单次遍历;md 缓存改有界 LRU(除满即全清抖动);useTurnTrace 补 never 穷尽 + default;MessageItem 模板去重
- 无障碍(四批累计): 可点容器键盘化(含 role/tabindex/keydown/aria-expanded) + icon-only aria-label 约 60 处 + 装饰图标 aria-hidden + 52 个滚动容器 overscroll-behavior:contain + 设置开关聚焦可见环 + outline 零补偿站点补 :focus-visible
- UX 走查项: 灵感 IDEA-10/11、知识 KNOW-6/7/9、项目详情 O8、审计 SA-4、设置 SA-6/26/27/28(脏表单/冗余 save/clamp/删除二次确认)
- 文件查看专项: 切工程状态残留 F5/F6、FileTree 去重与展开反馈 F7/F15/F17、切文件头部残留 F18、R 状态着色 F19;FilePreview diff 文件头(---/+++)误当增删渲染修复 + 大纲行高亮死样式修复 + 图片失败文案 i18n 化
- 同步 IO 收口(异步上下文内阻塞): unattended 看板读写改 tokio::fs、fetch_url 探活改 spawn_blocking、http/generate_image 清理改 tokio::fs
- 子代理 P3 批量: 任务书钉扎上限/截断文案区分父子语境/环境画像日期/temperature 可覆盖/model 回退上报/lang 透传
- crate 清理: df-execute·df-types 注释与格式、df-ideas 过时注释、df-nodes subflow 夸大描述、df-project 死变量
This commit is contained in:
lxy
2026-10-10 08:09:45 +08:00
parent 8df4e33656
commit 39e0eac39d
67 changed files with 1344 additions and 364 deletions
+53 -3
View File
@@ -82,7 +82,10 @@
<span v-else class="decided-none">—</span>
</td>
<td class="col-args"><code class="brief">{{ r.arguments_brief || '—' }}</code></td>
<td class="col-result"><code class="brief">{{ r.result_brief || '—' }}</code></td>
<td class="col-result">
<span v-if="hasOffloadMark(r.result_brief)" class="offload-chip">{{ t('aiTool.resultOffloaded') }}</span>
<code v-else class="brief">{{ r.result_brief || '—' }}</code>
</td>
</tr>
<tr v-if="expandedId === r.id" class="audit-detail-row">
<td colspan="7">
@@ -93,7 +96,18 @@
</div>
<div class="detail-section">
<span class="detail-label">{{ t('auditLog.col.result') }}</span>
<pre class="detail-pre">{{ r.result_brief || '—' }}</pre>
<!-- 外置句柄(D2a):显「结果已外置」+ 展开经 IPC 读回原文全文 -->
<template v-if="offloadHandle">
<OffloadedResultBar
:handle="offloadHandle"
:expanded="offloadExpanded"
:loading="offloadLoading"
:error="offloadError"
@toggle="toggleOffload"
/>
<pre v-if="offloadResolved != null" class="detail-pre">{{ offloadResolved }}</pre>
</template>
<pre v-else class="detail-pre">{{ r.result_brief || '—' }}</pre>
</div>
</div>
</td>
@@ -175,6 +189,8 @@ import { invoke } from '@tauri-apps/api/core'
import { formatRelative, formatDate } from '@/utils/time'
import { usePersistedRef } from '@/composables/usePersistedRef'
import { aiApi } from '@/api/ai'
import { hasOffloadMark, useOffloadedResult } from '@/composables/ai/useOffloadedResult'
import OffloadedResultBar from '@/components/OffloadedResultBar.vue'
import type { AiConversationSummary } from '@/api/types'
const { t } = useI18n()
@@ -255,6 +271,20 @@ function toggleExpand(id: string) {
expandedId.value = expandedId.value === id ? null : id
}
// 展开行的外置句柄详情(D2a):同一视图只展开一行,读回状态复用单实例;
// 切行时 reset 防串上一行的读回结果/错误。句柄来源=后端截断的 result_brief。
const expandedRecord = computed(() => records.value.find(r => r.id === expandedId.value) ?? null)
const {
handle: offloadHandle,
expanded: offloadExpanded,
loading: offloadLoading,
error: offloadError,
resolved: offloadResolved,
toggle: toggleOffload,
reset: resetOffload,
} = useOffloadedResult(() => expandedRecord.value?.result_brief ?? null)
watch(expandedId, () => resetOffload())
// 审批→执行 耗时(秒级,毫秒差太精细)
function durationLabel(requested: string, executed: string): string {
const ms = new Date(executed).getTime() - new Date(requested).getTime()
@@ -356,24 +386,32 @@ async function loadConversations() {
convLoading.value = false
}
}
// 对话详情请求序号守卫(对齐上方 _loadSeq 模式):快速切换对话时,
// 旧对话 export 响应晚到不覆盖新对话消息;折叠时自增使在途请求作废
let _convSeq = 0
async function toggleConv(id: string) {
if (expandedConv.value === id) {
_convSeq++ // 折叠:作废在途请求,避免其回填残留
expandedConv.value = null
convMessages.value = []
convLoading.value = false
return
}
const seq = ++_convSeq
expandedConv.value = id
convLoading.value = true
convError.value = ''
try {
const json = await aiApi.exportConversation(id, 'json')
if (seq !== _convSeq) return // 旧响应丢弃,只取最新
const parsed = JSON.parse(json) as ConvMessage[] ?? []
convMessages.value = parsed || []
} catch (e) {
if (seq !== _convSeq) return // 旧请求的失败不污染当前视图
convError.value = String(e)
convMessages.value = []
} finally {
convLoading.value = false
if (seq === _convSeq) convLoading.value = false
}
}
@@ -578,6 +616,18 @@ onMounted(() => {
white-space: pre-wrap;
}
/* D2a 外置句柄:结果列显标记替代截断 JSON(点行展开看详情并可读回全文) */
.offload-chip {
display: inline-block;
font-size: 10px;
font-weight: 600;
padding: 1px 7px;
border-radius: var(--df-radius-xs);
background: var(--df-warning-bg);
color: var(--df-warning);
white-space: nowrap;
}
/* 风险 badge */
.risk-badge {
display: inline-block;