新增: 待发箱抽屉与对话体验前端批
- 待发箱抽屉: ChatInput ⏰ 入口 + OutboxDrawer + useAiOutbox IPC 封装
- U2 进度语义三层: 内容层(最近动作) + 静默三档降速脉冲, 永不自动判死
- 流式心跳自愈: 竞态窗口旧任务迟到完成事件误收敛的自愈判定(949 前端兜底)
- 953 截断提示: 末条气泡渲染截断标记 + 继续输出按钮, 事件契约透传
This commit is contained in:
@@ -156,13 +156,22 @@
|
||||
<div class="ai-bottom-tools-right">
|
||||
<!-- 无人值守任务进度(AiTaskProgress,设计 §4.3):当前会话有进度快照时显示,
|
||||
静默期(子代理长跑/大工具执行/重入退避)凭最后快照可见「在干活」;终局即清。
|
||||
max_rounds=0(不限轮次)不显分母进度条,只显轮次与 token。
|
||||
max_rounds=0(不限轮次)不显分母进度条,只显轮次与 token(防假进度规则2:无分母禁画百分比)。
|
||||
U2 三层(设计 §三):活性=脉冲点(静默降速,永不伪装停止);位置=轮次/token 数字;
|
||||
内容=阶段标签(最近工具映射)+已完成步数,静默档附加判断材料(不自动判死,决策留给用户)。
|
||||
A8 双进度矛盾:输入区 agentic 实时进度条可见时(streaming 且 agentRound>0)
|
||||
隐藏本快照徽章——两处同屏且轮数口径不同(实时 vs 每5轮快照)互相矛盾;
|
||||
快照徽章只服务输入区不可见场景(托盘/单轮大工具静默期)。 -->
|
||||
<div v-if="taskProgress && !agenticProgressVisible" class="ai-task-progress">
|
||||
<div
|
||||
v-if="taskProgress && !agenticProgressVisible"
|
||||
class="ai-task-progress"
|
||||
:class="silenceTier !== 'normal' ? `ai-task-progress--${silenceTier}` : ''"
|
||||
:title="taskProgressHint"
|
||||
>
|
||||
<span class="ai-task-progress-dot"></span>
|
||||
<span class="ai-task-progress-text">{{ taskProgressText }}</span>
|
||||
<span v-if="stageText" class="ai-task-progress-stage">{{ stageText }}</span>
|
||||
<span v-if="silenceSuffix" class="ai-task-progress-silence">{{ silenceSuffix }}</span>
|
||||
<span
|
||||
v-if="taskProgress.maxRounds > 0"
|
||||
class="ai-task-progress-bar"
|
||||
@@ -589,6 +598,106 @@ const taskProgressPct = computed(() => {
|
||||
return Math.min(100, Math.round((p.round / p.maxRounds) * 100))
|
||||
})
|
||||
|
||||
// ── U2 进度语义三层补齐(设计 §三):内容层(最近动作)+静默期三档(§3.4) ──
|
||||
// 规则1 兑现:下方定时器只算「距上次信号多久」,永不推进任何计数(轮次/步数只由事件写)。
|
||||
// 规则4 兑现:活性=脉冲节奏(静默降速),位置=数字,分开渲染;永不自动判死(重入退避最长
|
||||
// 15min 是合法等待),警示档只给判断材料,「疑似卡死可停止」的决策留给用户。
|
||||
|
||||
// 内容层数据源:工具卡挂在 assistant 消息上(useAiToolEvents),从消息流纯派生——零新事件
|
||||
// 监听零协议改动。steps=当前消息流工具调用总数(压缩仅标记不清消息,计数不回退;清空上下文
|
||||
// 重置属用户显式重开,语义自洽);last 优先取 running 卡(当前动作),无 running 取最后一张。
|
||||
const toolActivity = computed(() => {
|
||||
let steps = 0
|
||||
let lastName: string | null = null
|
||||
let runningName: string | null = null
|
||||
for (const m of store.state.messages) {
|
||||
const tcs = m.toolCalls
|
||||
if (!tcs?.length) continue
|
||||
steps += tcs.length
|
||||
for (const tc of tcs) {
|
||||
lastName = tc.name
|
||||
if (tc.status === 'running') runningName = tc.name
|
||||
}
|
||||
}
|
||||
return { steps, last: runningName ?? lastName }
|
||||
})
|
||||
|
||||
// 阶段标签映射(设计 §3.3 同源词表桌面份):工具名→语义标签;未命中直显工具名
|
||||
const STAGE_KEYS: Record<string, string> = {
|
||||
read_file: 'read', read_symbol: 'read', search_files: 'read', list_directory: 'read',
|
||||
write_file: 'edit', patch_file: 'edit',
|
||||
run_command: 'run',
|
||||
spawn_agent: 'agent',
|
||||
}
|
||||
const STAGE_PREFIXES: [string, string][] = [['knowledge_', 'knowledge'], ['idea_', 'knowledge'], ['task_', 'knowledge']]
|
||||
function stageKeyOf(name: string): string | null {
|
||||
if (STAGE_KEYS[name]) return STAGE_KEYS[name]
|
||||
for (const [p, k] of STAGE_PREFIXES) if (name.startsWith(p)) return k
|
||||
return null
|
||||
}
|
||||
const stageText = computed(() => {
|
||||
const act = toolActivity.value
|
||||
if (!act.last) return ''
|
||||
const key = stageKeyOf(act.last)
|
||||
const parts = [key ? t(`ai.taskProgress.stage.${key}`) : act.last]
|
||||
if (act.steps > 0) parts.push(t('ai.taskProgress.steps', { n: act.steps }))
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
// 静默期计时:基准=最近一次真实信号(AiTaskProgress.updatedAt 或消息流指纹变化时刻)。
|
||||
// 指纹 watch 只在信号真实变化时刷新(规则5:同值重刷不产生新活性);文本增量/工具卡增减/
|
||||
// 轮次推进都会被捕获。
|
||||
const lastActivityAt = ref(Date.now())
|
||||
watch(() => {
|
||||
const msgs = store.state.messages
|
||||
const last = msgs[msgs.length - 1]
|
||||
return [msgs.length, last?.id, last?.content?.length, last?.toolCalls?.length, store.state.agentRound]
|
||||
}, () => { lastActivityAt.value = Date.now() })
|
||||
// 切会话用已知时间锚点重置(快照 updatedAt/末条消息时间),防「刚切回就看不到本就持续的静默」
|
||||
watch(() => store.state.activeConversationId, () => {
|
||||
const msgs = store.state.messages
|
||||
const last = msgs[msgs.length - 1]
|
||||
lastActivityAt.value = Math.max(taskProgress.value?.updatedAt ?? 0, last?.timestamp ?? 0)
|
||||
})
|
||||
// 15s 心跳只为刷新「距上次信号」展示(§3.4 档位阈值 2/10min,15s 粒度足够;规则1:定时器
|
||||
// 不碰任何计数)
|
||||
const nowTick = ref(Date.now())
|
||||
let silenceTimer: ReturnType<typeof setInterval> | null = null
|
||||
onMounted(() => {
|
||||
silenceTimer = setInterval(() => { nowTick.value = Date.now() }, 15_000)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (silenceTimer) { clearInterval(silenceTimer); silenceTimer = null }
|
||||
})
|
||||
const silenceSecs = computed(() => {
|
||||
const p = taskProgress.value
|
||||
if (!p) return 0
|
||||
return Math.max(0, Math.floor((nowTick.value - Math.max(p.updatedAt, lastActivityAt.value)) / 1000))
|
||||
})
|
||||
// §3.4 三档:<2min 正常 / 2-10min 长执行(脉冲降速) / >10min 警示(给判断材料不自动判死)
|
||||
const silenceTier = computed<'normal' | 'long' | 'warn'>(() => {
|
||||
const s = silenceSecs.value
|
||||
if (s >= 600) return 'warn'
|
||||
if (s >= 120) return 'long'
|
||||
return 'normal'
|
||||
})
|
||||
const silenceSuffix = computed(() => {
|
||||
if (!taskProgress.value) return ''
|
||||
if (silenceTier.value === 'warn') return t('ai.taskProgress.silentShort', { n: Math.floor(silenceSecs.value / 60) })
|
||||
if (silenceTier.value === 'long') return t('ai.taskProgress.longRun')
|
||||
return ''
|
||||
})
|
||||
// 悬浮提示:警示档给完整判断材料(距上次活动 n 分钟+最后动作+出路),长执行档给成因提示,
|
||||
// 正常档不弹(位置信息正文已有,避免无意义提示)
|
||||
const taskProgressHint = computed(() => {
|
||||
if (!taskProgress.value) return ''
|
||||
if (silenceTier.value === 'warn') {
|
||||
return t('ai.taskProgress.silentWarn', { n: Math.floor(silenceSecs.value / 60), tool: toolActivity.value.last ?? '-' })
|
||||
}
|
||||
if (silenceTier.value === 'long') return t('ai.taskProgress.longRunHint')
|
||||
return ''
|
||||
})
|
||||
|
||||
// 🎯 对话目标面板(对话透明化 L1:Goal visibility)
|
||||
const goals = ref<GoalEntry[]>([])
|
||||
const goalsExpanded = ref(false)
|
||||
@@ -1077,6 +1186,20 @@ onBeforeUnmount(() => {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
/* U2 静默期档位(设计 §3.4):长执行/警示=脉冲降速——活性层表达「慢」而非伪装「停」,
|
||||
不自动判死;内容层/静默档文字用弱化色,视觉从属位置层数字 */
|
||||
.ai-task-progress--long .ai-task-progress-dot,
|
||||
.ai-task-progress--warn .ai-task-progress-dot {
|
||||
animation-duration: 3.2s;
|
||||
}
|
||||
.ai-task-progress-stage {
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
.ai-task-progress-silence {
|
||||
font-size: 11px;
|
||||
color: color-mix(in srgb, var(--df-warning) 70%, var(--df-text-secondary));
|
||||
}
|
||||
/* ═══ Provider Bar (header 中间区;div→button 后归零默认样式) ═══ */
|
||||
.ai-provider-bar {
|
||||
display: inline-flex;
|
||||
|
||||
Reference in New Issue
Block a user