新增: aichat前端体验四件与队列残留收口
- R4 代码块语言角标:MessageList decorateCodeHtml 捕获 language-xxx 输出 data-lang+CSS ::before 左上角标,hover 显,无语言裸块不渲染 - R6 工具结果路径可复制:ToolResultBody 五处 file-bar 的 path span→button(read_file/list_directory/search_files/grep/patch_file),点击复制完整路径+✓/✕ 1.2s 反馈+execCommand 降级链 - I6 重新生成 UI 入口:MessageItem user 气泡 hover 补重新生成按钮(canRegenerate=isLastUser&&非流式),复用 handleRetry→store.regenerate 链路 - V4 浮窗审批 High 风险二次确认:useToolApproval 导出 isHighRiskTool/highRiskConfirmText,ApprovalPopup.onApprove 前置 ConfirmDialog,数字键守卫不绕确认 - 9-28#5 两处全局清队列残留:useAiEvents/useAiLifecycleEvents 无 convId 的 AiError 分支改保留队列+console.warn(防误伤其他会话排队消息) - i18n zh/en 补 clickToCopy 键;裸 @ 改 @ 转义(vue-i18n linked-message 前缀吞噬)
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<div v-if="tc.name === 'read_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-file-content">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon" v-html="fileIcon"></span>
|
||||
<span class="ai-tool-file-path" :title="parsed?.path">{{ shortPath(parsed?.path || '') }}</span>
|
||||
<button type="button" class="ai-tool-file-path ai-tool-file-path--copy" :title="parsed?.path + ' — ' + $t('aiTool.clickToCopy')" @click.stop="copyResultPath($event, parsed?.path || '')">{{ shortPath(parsed?.path || '') }}</button>
|
||||
<!-- BUG-260625-02: search 模式返回 matches(命中行)非 content,
|
||||
旧模板取 lines/returned_lines 显「0 行」+ 空 content 区,用户误以为读取失败。
|
||||
search 模式显搜索词 + 命中数(复用 grepHitsN 文案);默认模式显行数/大小 + 展开。 -->
|
||||
@@ -36,7 +36,7 @@
|
||||
<div v-else-if="tc.name === 'list_directory' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-dir-list">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon ai-tool-file-icon--dir" v-html="dirIcon"></span>
|
||||
<span class="ai-tool-file-path" :title="parsed?.path">{{ shortPath(parsed?.path || '') }}</span>
|
||||
<button type="button" class="ai-tool-file-path ai-tool-file-path--copy" :title="parsed?.path + ' — ' + $t('aiTool.clickToCopy')" @click.stop="copyResultPath($event, parsed?.path || '')">{{ shortPath(parsed?.path || '') }}</button>
|
||||
<span class="ai-tool-file-meta">{{ parsed?.entries?.length || 0 }} {{ $t('aiTool.items') }}</span>
|
||||
<span v-if="parsed?.truncated" class="ai-tool-file-meta ai-tool-file-meta--warn">{{ $t('aiTool.dirTruncated', { n: DIR_TRUNCATE_LIMIT }) }}</span>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@
|
||||
<div v-else-if="tc.name === 'search_files' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-dir-list">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon ai-tool-file-icon--dir" v-html="dirIcon"></span>
|
||||
<span class="ai-tool-file-path" :title="parsed?.path">{{ shortPath(parsed?.path || '') }}</span>
|
||||
<button type="button" class="ai-tool-file-path ai-tool-file-path--copy" :title="parsed?.path + ' — ' + $t('aiTool.clickToCopy')" @click.stop="copyResultPath($event, parsed?.path || '')">{{ shortPath(parsed?.path || '') }}</button>
|
||||
<span v-if="parsed?.pattern" class="ai-tool-file-meta">「{{ parsed?.pattern }}」</span>
|
||||
<span class="ai-tool-file-meta">{{ $t('aiTool.foundN', { n: typeof parsed?.total === 'number' ? parsed.total : (parsed?.results?.length || 0) }) }}</span>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div v-else-if="tc.name === 'grep' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-grep-result">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon" v-html="searchIcon"></span>
|
||||
<span class="ai-tool-file-path" :title="parsed?.path">{{ shortPath(parsed?.path || '') }}</span>
|
||||
<button type="button" class="ai-tool-file-path ai-tool-file-path--copy" :title="parsed?.path + ' — ' + $t('aiTool.clickToCopy')" @click.stop="copyResultPath($event, parsed?.path || '')">{{ shortPath(parsed?.path || '') }}</button>
|
||||
<span v-if="parsed?.pattern" class="ai-tool-file-meta">「{{ parsed?.pattern }}」</span>
|
||||
<span class="ai-tool-file-meta">{{ grepHitLabel }}</span>
|
||||
<span v-if="parsed?.truncated" class="ai-tool-file-meta ai-tool-file-meta--warn">{{ $t('aiTool.grepTruncated') }}</span>
|
||||
@@ -125,7 +125,7 @@
|
||||
<div v-else-if="tc.name === 'patch_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-patch-result">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon" v-html="fileIcon"></span>
|
||||
<span class="ai-tool-file-path" :title="parsed?.path">{{ shortPath(parsed?.path || '') }}</span>
|
||||
<button type="button" class="ai-tool-file-path ai-tool-file-path--copy" :title="parsed?.path + ' — ' + $t('aiTool.clickToCopy')" @click.stop="copyResultPath($event, parsed?.path || '')">{{ shortPath(parsed?.path || '') }}</button>
|
||||
<span class="ai-tool-file-meta">{{ formatSizeDiff(parsed?.size_diff) }} {{ $t('aiTool.bytesUnit') }}</span>
|
||||
<span v-if="parsed?.matches_found && parsed.matches_found > 1" class="ai-tool-file-meta">{{ $t('aiTool.patchedMatches', { n: parsed.matches_found }) }}</span>
|
||||
</div>
|
||||
@@ -257,6 +257,39 @@ const emit = defineEmits<{
|
||||
'install-obscura': []
|
||||
}>()
|
||||
|
||||
// ── R6 结果路径可复制 ──
|
||||
// AI 读过/写过的文件,用户想拿去本地操作(编辑器打开/再 grep)只能手动重敲路径。
|
||||
// file-bar 的 path 改为可点击:复制完整路径(parsed.path 原文,非 shortPath 截断展示),
|
||||
// 按钮文字短暂变「✓」作反馈(1.2s 还原,同 R4 代码块复制反馈节奏)。
|
||||
let _pathCopiedTimer: ReturnType<typeof setTimeout> | null = null
|
||||
async function copyResultPath(e: MouseEvent, path: string): Promise<void> {
|
||||
if (!path) return
|
||||
const btn = e.currentTarget as HTMLButtonElement | null
|
||||
let ok = false
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(path)
|
||||
ok = true
|
||||
}
|
||||
} catch { /* 落 execCommand 兜底 */ }
|
||||
if (!ok) {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = path
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try { ok = document.execCommand('copy') } catch { ok = false }
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
if (btn) {
|
||||
const prev = btn.textContent
|
||||
btn.textContent = ok ? '✓' : '✕'
|
||||
if (_pathCopiedTimer) clearTimeout(_pathCopiedTimer)
|
||||
_pathCopiedTimer = setTimeout(() => { btn.textContent = prev }, 1200)
|
||||
}
|
||||
}
|
||||
|
||||
// run_command 输出折叠态:失败命令默认展开(便于看 stderr),成功默认折叠
|
||||
const cmdOutputExpanded = ref(false)
|
||||
// http_request 响应 body 折叠态:失败(非 2xx)默认展开便于看错误,成功默认折叠
|
||||
@@ -403,6 +436,14 @@ watch(() => props.tc.status, (s) => {
|
||||
.ai-tool-file-meta--warn {
|
||||
color: var(--df-warning);
|
||||
}
|
||||
/* R6:路径可点击复制(按钮语义,视觉保持原 span 形态;hover 变 accent 提示可点) */
|
||||
.ai-tool-file-path--copy {
|
||||
cursor: pointer;
|
||||
transition: color 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-tool-file-path--copy:hover {
|
||||
color: var(--df-accent);
|
||||
}
|
||||
|
||||
/* grep 结果文件栏复用 ai-tool-file-bar,命中行用等宽渲染 */
|
||||
.ai-tool-grep-result {
|
||||
|
||||
Reference in New Issue
Block a user