新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
678
src/components/ToolCard.vue
Normal file
678
src/components/ToolCard.vue
Normal file
@@ -0,0 +1,678 @@
|
||||
<template>
|
||||
<div
|
||||
class="ai-tool-card"
|
||||
:class="['ai-tool-card--' + tc.status, { 'ai-tool-card--collapsed': !isExpanded && !shouldKeepOpen(tc) }]"
|
||||
>
|
||||
<!-- 卡片头部:图标 + 名称 + 状态 -->
|
||||
<div class="ai-tool-header" @click="emit('toggle', tc.id)">
|
||||
<span class="ai-tool-icon-wrap" :class="'ai-tool-icon-wrap--' + toolCategory(tc.name)">
|
||||
<span v-html="toolIcon(tc.name)"></span>
|
||||
</span>
|
||||
<div class="ai-tool-header-text">
|
||||
<span class="ai-tool-name">{{ toolDisplayName(tc) }}</span>
|
||||
<span v-if="tc.status === 'running'" class="ai-tool-sub">{{ $t('aiTool.running') }}</span>
|
||||
<span v-else-if="toolResultSummary(tc)" class="ai-tool-sub">{{ toolResultSummary(tc) }}</span>
|
||||
</div>
|
||||
<span class="ai-tool-status-dot" :class="'ai-tool-status-dot--' + tc.status"></span>
|
||||
<span v-if="!shouldKeepOpen(tc)" class="ai-tool-collapse-hint" :class="{ 'is-open': isExpanded }">▸</span>
|
||||
</div>
|
||||
|
||||
<!-- Body: 收起态隐藏(running/pending 强制展开) -->
|
||||
<div v-show="isExpanded || shouldKeepOpen(tc)" class="ai-tool-body">
|
||||
|
||||
<!-- Running 骨架屏 -->
|
||||
<div v-if="tc.status === 'running'" class="ai-tool-skeleton">
|
||||
<div class="ai-tool-skeleton-line" style="width:60%"></div>
|
||||
<div class="ai-tool-skeleton-line" style="width:85%"></div>
|
||||
<div class="ai-tool-skeleton-line" style="width:40%"></div>
|
||||
</div>
|
||||
|
||||
<!-- 审批:参数内容 + 风险提示 + 按钮 -->
|
||||
<div v-if="tc.status === 'pending_approval'" class="ai-tool-approval">
|
||||
<div v-if="toolArgsEntries(tc.args).length" class="ai-tool-approval-args">
|
||||
<div v-for="arg in toolArgsEntries(tc.args)" :key="arg.key" class="ai-tool-arg">
|
||||
<span class="ai-tool-arg-key">{{ arg.key }}</span>
|
||||
<span class="ai-tool-arg-val">{{ formatArgValue(arg.val) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tc.reason" class="ai-tool-approval-reason">⚠ {{ tc.reason }}</div>
|
||||
<div class="ai-tool-actions">
|
||||
<button class="ai-tool-btn ai-tool-btn--approve" @click="emit('approve', { id: tc.id, approved: true })">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
{{ $t('aiTool.approve') }}
|
||||
</button>
|
||||
<button class="ai-tool-btn ai-tool-btn--reject" @click="emit('approve', { id: tc.id, approved: false })">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
{{ $t('aiTool.reject') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已拒绝提示 -->
|
||||
<div v-if="tc.status === 'rejected'" class="ai-tool-rejected">
|
||||
<span class="ai-tool-rejected-icon">✕</span>
|
||||
{{ tc.result || $t('aiTool.rejectedHint') }}
|
||||
</div>
|
||||
|
||||
<!-- read_file 结果:代码预览 -->
|
||||
<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">{{ parsed?.path }}</span>
|
||||
<span class="ai-tool-file-meta">{{ parsed?.lines || 0 }} {{ $t('aiTool.lines') }} · {{ formatBytes(parsed?.size) }}</span>
|
||||
<button class="ai-tool-expand-btn" @click="emit('expand-content', tc.id)">
|
||||
{{ isContentExpanded ? $t('aiTool.collapse') : $t('aiTool.expandAll') }}
|
||||
</button>
|
||||
</div>
|
||||
<pre class="ai-tool-file-pre" :class="{ 'ai-tool-file-pre--collapsed': !isContentExpanded }"><code>{{ parsed?.content }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- list_directory 结果:文件树 -->
|
||||
<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">{{ parsed?.path }}</span>
|
||||
<span class="ai-tool-file-meta">{{ parsed?.entries?.length || 0 }} {{ $t('aiTool.items') }}</span>
|
||||
</div>
|
||||
<div class="ai-tool-dir-entries">
|
||||
<div
|
||||
v-for="(entry, i) in parsed?.entries"
|
||||
:key="i"
|
||||
class="ai-tool-dir-entry"
|
||||
:class="'ai-tool-dir-entry--' + entry.type"
|
||||
:style="{ paddingLeft: `${12 + (entry.depth || 0) * 16}px` }"
|
||||
>
|
||||
<span class="ai-tool-dir-icon" :class="entry.type === 'directory' ? 'ai-tool-dir-icon--folder' : 'ai-tool-dir-icon--file'" v-html="entry.type === 'directory' ? dirIcon : fileIcon"></span>
|
||||
<span class="ai-tool-dir-name">{{ entry.name }}</span>
|
||||
<span v-if="entry.type === 'file'" class="ai-tool-dir-size">{{ formatBytes(entry.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- write_file 结果 -->
|
||||
<div v-else-if="tc.name === 'write_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-write-result">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--df-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
<span class="ai-tool-write-path">{{ parsed?.path }}</span>
|
||||
<span class="ai-tool-write-meta">{{ formatBytes(parsed?.bytes_written) }} {{ $t('aiTool.written') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 通用 CRUD 结果 -->
|
||||
<div v-else-if="tc.result && tc.status === 'completed'" class="ai-tool-result">
|
||||
<code>{{ formatToolResult(tc) }}</code>
|
||||
</div>
|
||||
</div><!-- /.ai-tool-body -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { AiToolCallInfo } from '../api/types'
|
||||
|
||||
/** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */
|
||||
interface ToolResult {
|
||||
path?: string
|
||||
content?: string
|
||||
lines?: number
|
||||
size?: number
|
||||
bytes_written?: number
|
||||
entries?: Array<{ name: string; type: string; size?: number; depth?: number }>
|
||||
name?: string
|
||||
title?: string
|
||||
field?: string
|
||||
deleted?: boolean
|
||||
}
|
||||
|
||||
/** 内联 SVG 图标字符串(经 v-html 注入,常量安全;图标库改造见决策记录) */
|
||||
const dirIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>'
|
||||
const fileIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'
|
||||
|
||||
/** 默认保持展开:执行中/待审批/已拒绝/write_file——折叠无收益 */
|
||||
function shouldKeepOpen(tc: AiToolCallInfo): boolean {
|
||||
return tc.status === 'running'
|
||||
|| tc.status === 'pending_approval'
|
||||
|| tc.status === 'rejected'
|
||||
|| tc.name === 'write_file'
|
||||
}
|
||||
|
||||
function formatToolName(name: string): string {
|
||||
return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
|
||||
function formatJson(data: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(data, null, 2)
|
||||
} catch {
|
||||
return String(data)
|
||||
}
|
||||
}
|
||||
|
||||
/** 审批参数转键值对数组(非对象/空时返回空数组,防 args 非 object 时 v-for 报错) */
|
||||
function toolArgsEntries(args: unknown): Array<{ key: string; val: unknown }> {
|
||||
if (args && typeof args === 'object' && !Array.isArray(args)) {
|
||||
return Object.entries(args as Record<string, unknown>).map(([key, val]) => ({ key, val }))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/** 格式化单个审批参数值:字符串直接展示(超长截断),对象/数组 JSON 化后截断 */
|
||||
function formatArgValue(val: unknown): string {
|
||||
if (val == null) return ''
|
||||
if (typeof val === 'string') return val.length > 300 ? val.slice(0, 300) + '…' : val
|
||||
if (typeof val === 'object') {
|
||||
try {
|
||||
const s = JSON.stringify(val)
|
||||
return s.length > 300 ? s.slice(0, 300) + '…' : s
|
||||
} catch {
|
||||
return String(val)
|
||||
}
|
||||
}
|
||||
return String(val)
|
||||
}
|
||||
|
||||
/** 安全解析工具结果 JSON;list_* 运行时是数组,类型归 ToolResult 靠 Array.isArray 区分 */
|
||||
function parseResult(result: unknown): ToolResult | null {
|
||||
if (!result) return null
|
||||
try {
|
||||
return (typeof result === 'string' ? JSON.parse(result) : result) as ToolResult
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function toolCategory(name: string): string {
|
||||
if (name === 'read_file') return 'file-read'
|
||||
if (name === 'write_file') return 'file-write'
|
||||
if (name === 'list_directory') return 'dir'
|
||||
if (name.includes('create')) return 'create'
|
||||
if (name.includes('delete')) return 'delete'
|
||||
if (name.includes('list')) return 'list'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function toolIcon(name: string): string {
|
||||
if (name === 'read_file') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
|
||||
if (name === 'write_file') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'
|
||||
if (name === 'list_directory') return dirIcon
|
||||
if (name.includes('create')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
|
||||
if (name.includes('delete')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>'
|
||||
return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
|
||||
}
|
||||
|
||||
function shortPath(p: string): string {
|
||||
const parts = p.replace(/\\/g, '/').split('/')
|
||||
return parts.length <= 2 ? p : '.../' + parts.slice(-2).join('/')
|
||||
}
|
||||
|
||||
/** 格式化字节数 */
|
||||
function formatBytes(bytes: number | undefined): string {
|
||||
if (!bytes) return '0 B'
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / 1048576).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
/** 通用工具结果格式化(非文件类工具) */
|
||||
function formatToolResult(tc: AiToolCallInfo): string {
|
||||
const r = parseResult(tc.result)
|
||||
if (!r) {
|
||||
// 非 JSON(纯文本:审批占位/错误/拒绝)→ 原样显示,审批占位时补拟执行参数避免空白
|
||||
const raw = typeof tc.result === 'string' ? tc.result : ''
|
||||
if (raw && tc.args && typeof tc.args === 'object') {
|
||||
const a = tc.args as any
|
||||
const detail = a.title || a.name || a.path || ''
|
||||
if (detail) return `${raw}(${detail})`
|
||||
}
|
||||
return raw
|
||||
}
|
||||
if (tc.name === 'write_file') {
|
||||
return `${r.path} (${formatBytes(r.bytes_written)})`
|
||||
}
|
||||
return formatJson(r)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
/** 单条工具调用数据 */
|
||||
tc: AiToolCallInfo
|
||||
/** 卡片级展开(来自父级 expandedCards) */
|
||||
isExpanded: boolean
|
||||
/** 内容级展开(read_file 的"展开全部",来自父级 expandedTools) */
|
||||
isContentExpanded: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** header 点击 → 卡片折叠/展开 */
|
||||
toggle: [id: string]
|
||||
/** read_file "展开全部/收起" → 内容级展开 */
|
||||
'expand-content': [id: string]
|
||||
/** 审批按钮 → 父级转发 store.approveToolCall */
|
||||
approve: [{ id: string; approved: boolean }]
|
||||
}>()
|
||||
|
||||
/** 一次解析结果供模板多次读(模板引用 6 次,computed 避免每次 patch 重 parse) */
|
||||
const parsed = computed(() => parseResult(props.tc.result))
|
||||
|
||||
/** 工具显示名称(含路径摘要);无 path 时硬编码 fallback(path 几乎总有,i18n 兜底属过度防御) */
|
||||
function toolDisplayName(tc: AiToolCallInfo): string {
|
||||
const p = (tc.args as any)?.path as string | undefined
|
||||
switch (tc.name) {
|
||||
case 'read_file': return p ? `${t('aiTool.readPrefix')} ${shortPath(p)}` : t('aiTool.readFallback')
|
||||
case 'list_directory': return p ? `${t('aiTool.dirPrefix')} ${shortPath(p)}` : t('aiTool.dirFallback')
|
||||
case 'write_file': return p ? `${t('aiTool.writePrefix')} ${shortPath(p)}` : t('aiTool.writeFallback')
|
||||
default: return formatToolName(tc.name)
|
||||
}
|
||||
}
|
||||
|
||||
/** completed 工具结果的一句话摘要(折叠态 header 也能看到,避免光秃秃) */
|
||||
function toolResultSummary(tc: AiToolCallInfo): string {
|
||||
if (tc.status !== 'completed') return ''
|
||||
const r = parseResult(tc.result)
|
||||
if (!r) return ''
|
||||
switch (tc.name) {
|
||||
case 'list_tasks': return Array.isArray(r) ? t('aiTool.taskCount', { n: r.length }) : ''
|
||||
case 'list_projects': return Array.isArray(r) ? t('aiTool.projectCount', { n: r.length }) : ''
|
||||
case 'list_ideas': return Array.isArray(r) ? t('aiTool.ideaCount', { n: r.length }) : ''
|
||||
case 'create_project': return r.name ? t('aiTool.createdWithName', { name: r.name }) : t('aiTool.created')
|
||||
case 'create_idea':
|
||||
case 'create_task': return r.title ? t('aiTool.createdWithName', { name: r.title }) : t('aiTool.created')
|
||||
case 'update_project': return r.field ? t('aiTool.updatedField', { field: r.field }) : t('aiTool.updated')
|
||||
case 'delete_project': return r.deleted ? t('aiTool.deleted') : t('aiTool.deleteIneffective')
|
||||
case 'run_workflow': return t('aiTool.workflowHint')
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ai-tool-card {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
max-width: var(--df-msg-max-width);
|
||||
animation: fadeInUp 0.2s var(--df-ease);
|
||||
transition: border-color 0.2s var(--df-ease);
|
||||
}
|
||||
.ai-tool-card--pending_approval {
|
||||
border-color: rgba(240,199,94,0.3);
|
||||
box-shadow: 0 0 0 1px rgba(240,199,94,0.06);
|
||||
}
|
||||
.ai-tool-card--completed {
|
||||
border-color: rgba(61,219,160,0.2);
|
||||
}
|
||||
.ai-tool-card--rejected {
|
||||
border-color: rgba(240,101,101,0.2);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.ai-tool-rejected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px 8px 42px;
|
||||
font-size: 11px;
|
||||
color: var(--df-danger);
|
||||
}
|
||||
.ai-tool-rejected-icon {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* -- 卡片折叠态(running/pending 始终展开,completed/rejected 默认折叠为单行 header) -- */
|
||||
.ai-tool-card--collapsed .ai-tool-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ai-tool-card--collapsed .ai-tool-header:hover {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
.ai-tool-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-tool-collapse-hint {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-tool-collapse-hint.is-open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* -- Header -- */
|
||||
.ai-tool-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.ai-tool-icon-wrap {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-tool-icon-wrap--file-read { background: rgba(94,175,240,0.12); color: var(--df-info); }
|
||||
.ai-tool-icon-wrap--file-write { background: var(--df-warning-bg); color: var(--df-warning); }
|
||||
.ai-tool-icon-wrap--dir { background: rgba(240,199,94,0.12); color: var(--df-warning); }
|
||||
.ai-tool-icon-wrap--create { background: var(--df-success-bg); color: var(--df-success); }
|
||||
.ai-tool-icon-wrap--delete { background: var(--df-danger-bg); color: var(--df-danger); }
|
||||
.ai-tool-icon-wrap--list { background: var(--df-accent-bg); color: var(--df-accent); }
|
||||
.ai-tool-icon-wrap--default { background: var(--df-accent-bg); color: var(--df-accent); }
|
||||
|
||||
.ai-tool-header-text {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
column-gap: 8px;
|
||||
row-gap: 1px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ai-tool-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--df-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-tool-sub {
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* -- Status dot (脉冲动画) -- */
|
||||
.ai-tool-status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-tool-status-dot--running {
|
||||
background: var(--df-info);
|
||||
animation: pulseGlow 1.5s ease-in-out infinite;
|
||||
box-shadow: 0 0 6px var(--df-info);
|
||||
}
|
||||
.ai-tool-status-dot--pending_approval {
|
||||
background: var(--df-warning);
|
||||
animation: pulseGlow 1.5s ease-in-out infinite;
|
||||
box-shadow: 0 0 6px rgba(240,199,94,0.3);
|
||||
}
|
||||
.ai-tool-status-dot--completed { background: var(--df-success); }
|
||||
.ai-tool-status-dot--rejected { background: var(--df-danger); }
|
||||
|
||||
/* -- Skeleton (running 状态) -- */
|
||||
.ai-tool-skeleton {
|
||||
padding: 4px 10px 10px 42px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.ai-tool-skeleton-line {
|
||||
height: 8px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: linear-gradient(90deg, var(--df-bg) 25%, var(--df-bg-card-hover) 50%, var(--df-bg) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* -- 审批参数展示 -- */
|
||||
.ai-tool-approval-args {
|
||||
padding: 4px 10px 6px;
|
||||
margin-bottom: 6px;
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
background: var(--df-bg-elevated, rgba(0, 0, 0, 0.03));
|
||||
}
|
||||
.ai-tool-arg {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ai-tool-arg-key {
|
||||
flex-shrink: 0;
|
||||
min-width: 64px;
|
||||
color: var(--df-text-secondary);
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.ai-tool-arg-val {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--df-font-mono);
|
||||
color: var(--df-text-primary);
|
||||
}
|
||||
.ai-tool-approval-reason {
|
||||
padding: 4px 10px 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
color: var(--df-warning, #d97706);
|
||||
background: var(--df-warning-bg, rgba(217, 119, 6, 0.08));
|
||||
border-radius: var(--df-radius-sm, 4px);
|
||||
}
|
||||
|
||||
/* -- 审批按钮 -- */
|
||||
.ai-tool-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 0 10px 8px;
|
||||
}
|
||||
.ai-tool-btn {
|
||||
padding: 5px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--df-radius-sm);
|
||||
border: 0.5px solid var(--df-border);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
font-family: var(--df-font-sans);
|
||||
}
|
||||
.ai-tool-btn--approve {
|
||||
background: var(--df-success-bg);
|
||||
color: var(--df-success);
|
||||
}
|
||||
.ai-tool-btn--approve:hover {
|
||||
background: var(--df-success);
|
||||
color: #fff;
|
||||
border-color: var(--df-success);
|
||||
}
|
||||
.ai-tool-btn--reject {
|
||||
background: var(--df-danger-bg);
|
||||
color: var(--df-danger);
|
||||
}
|
||||
.ai-tool-btn--reject:hover {
|
||||
background: var(--df-danger);
|
||||
color: #fff;
|
||||
border-color: var(--df-danger);
|
||||
}
|
||||
|
||||
/* -- File Content (read_file) -- */
|
||||
.ai-tool-file-content {
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
}
|
||||
.ai-tool-file-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
background: color-mix(in srgb, var(--df-bg) 80%, transparent);
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
}
|
||||
.ai-tool-file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--df-info);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-tool-file-icon--dir {
|
||||
color: var(--df-warning);
|
||||
}
|
||||
.ai-tool-file-path {
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--df-accent);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-tool-file-meta {
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 9px;
|
||||
color: var(--df-text-dim);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-tool-expand-btn {
|
||||
font-family: var(--df-font-sans);
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s var(--df-ease);
|
||||
}
|
||||
.ai-tool-expand-btn:hover {
|
||||
color: var(--df-text-secondary);
|
||||
background: rgba(255,255,255,0.08);
|
||||
border-color: var(--df-border-strong);
|
||||
}
|
||||
.ai-tool-file-pre {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
background: color-mix(in srgb, var(--df-bg) 60%, transparent);
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
color: var(--df-text-secondary);
|
||||
overflow-x: auto;
|
||||
tab-size: 4;
|
||||
}
|
||||
.ai-tool-file-pre--collapsed {
|
||||
max-height: 180px;
|
||||
/* hidden→auto: 默认高度内可直接滚轮看完整内容,不必先点"展开全部" */
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
.ai-tool-file-pre--collapsed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 48px;
|
||||
background: linear-gradient(transparent, color-mix(in srgb, var(--df-bg) 90%, transparent));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* -- Directory List (list_directory) -- */
|
||||
.ai-tool-dir-list {
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
}
|
||||
.ai-tool-dir-entries {
|
||||
padding: 4px 0;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ai-tool-dir-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 3px 12px;
|
||||
font-size: 11px;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.ai-tool-dir-entry:hover {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
.ai-tool-dir-entry--directory .ai-tool-dir-name {
|
||||
font-weight: 500;
|
||||
color: var(--df-text);
|
||||
}
|
||||
.ai-tool-dir-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ai-tool-dir-icon--folder { color: var(--df-warning); }
|
||||
.ai-tool-dir-icon--file { color: var(--df-text-dim); }
|
||||
.ai-tool-dir-name {
|
||||
font-family: var(--df-font-mono);
|
||||
color: var(--df-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
.ai-tool-dir-size {
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 9px;
|
||||
color: var(--df-text-dim);
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* -- Write File Result -- */
|
||||
.ai-tool-write-result {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
background: rgba(61,219,160,0.04);
|
||||
}
|
||||
.ai-tool-write-path {
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--df-accent);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-tool-write-meta {
|
||||
font-size: 10px;
|
||||
color: var(--df-success);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* -- Generic Result -- */
|
||||
.ai-tool-result {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
background: rgba(61,219,160,0.04);
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
border-radius: 0;
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--df-success);
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user