优化: 清理无意义注释(走查编号前缀/过时历史标注/死代码注释)

This commit is contained in:
lxy
2026-08-15 18:05:43 +08:00
parent 5a4c821629
commit 5ac815b9ca
137 changed files with 735 additions and 737 deletions
+4 -4
View File
@@ -101,7 +101,7 @@ watch(() => projectStore.error, (err) => {
errorTimer = setTimeout(() => { errorMsg.value = '' }, 3500)
})
// ── 人工审批对话框(全局,P0-C:工作流推进到 HumanNode 审批时,任意页面都应能响应)──
// ── 人工审批对话框(全局,工作流推进到 HumanNode 审批时,任意页面都应能响应)──
// 原 ProjectDetail 局部渲染 → TaskDetail 无 ApprovalDialog 致 store.pendingApproval 被设但
// 用户在 TaskDetail 页无法操作 → HumanNode 超时失败。提到 App.vue 全局化:任何路由响应审批。
// (系统级浮窗 useApprovalPopup 是窗口失焦时的提示;此处是聚焦时弹的对话框,二者职责互补)
@@ -174,7 +174,7 @@ onMounted(async () => {
await migrateLegacyLocalStorage()
// 3. 缓存已就绪,初始化主题/locale
initTheme()
// F-260618-23: 根 onMounted 恢复 Agentic 轮次/重试到后端(AtomicUsize 纯内存,重启回默认)。
// 根 onMounted 恢复 Agentic 轮次/重试到后端(AtomicUsize 纯内存,重启回默认)。
// GeneralPanel.vue onMounted 仅 Settings 页挂载,用户直接进 AI Chat 不同步 → 后端默认 10 与前端显示持久值不一致。
// clamp 与 GeneralPanel 语义对齐(0=不限透传,1-50 clamp;双保险,IPC 幂等值相同无害)
// 包 try/catch 防 IPC 失败 reject onMounted(对齐 dataChangedListener 模式)
@@ -201,8 +201,8 @@ onMounted(async () => {
window.addEventListener('keydown', handleKeydown)
// 加载知识库候选数(供侧栏 knowledge badge 显示)
knowledgeStore.loadCandidates()
// AR-11:数据变更联动监听(AI 工具 create/update/delete 后端 emit df-data-changed → store listen 刷新对应列表,全局挂载因用户可在任意页)
// CR-260615-21:包 try/catch 防 Tauri listen 失败 reject onMounted 致 AR-11 静默失效
// 数据变更联动监听(AI 工具 create/update/delete 后端 emit df-data-changed → store listen 刷新对应列表,全局挂载因用户可在任意页)
// 包 try/catch 防 Tauri listen 失败 reject onMounted 致 AR-11 静默失效
try {
await projectStore.startDataChangedListener()
} catch (e) {
+5 -5
View File
@@ -43,7 +43,7 @@ export const aiApi = {
},
/**
* 重新生成最后一条 AI 回复(UX-02)。
* 重新生成最后一条 AI 回复。
* 后端:占用 generating → 弹出末尾 AI 回复(保留触发它的 user 消息)→ 重跑 agentic loop。
* 前端消费:与 sendMessage 一样需本地 push 空气泡占位 + 置 streaming(见 useAiSend.regenerate)。
*/
@@ -56,7 +56,7 @@ export const aiApi = {
},
/**
* 编辑最后一条 user 消息并重新生成(UX-09)。
* 编辑最后一条 user 消息并重新生成。
* 后端:占用 generating → 替换末条 user content → 其后消息标 truncated(软删)→ 重跑 agentic loop。
* 前端消费:调 editMessage 前需移除被编辑消息之后的所有消息(视图截断)+ push 空气泡占位 + 置 streaming。
* 中间 user 消息编辑被后端拒绝(只允许末条)。
@@ -205,7 +205,7 @@ export const aiApi = {
},
/**
* 一次性测试连接(P0-1):用**表单当前值**测试,不写库。
* 一次性测试连接用**表单当前值**测试,不写库。
* - 新建态:传表单填的 providerType/baseUrl/apiKey,providerId=null。
* - 编辑态(apiKey 留空=不改):传 providerId 让后端从 DB 沿用旧密钥;
* apiKey 非空则直接用表单新值。
@@ -329,13 +329,13 @@ export const aiApi = {
return invoke('ai_conversation_archive', { conversationId, archived })
},
/** 置顶/取消置顶对话(UX-17) */
/** 置顶/取消置顶对话*/
setPinnedConversation(conversationId: string, pinned: boolean): Promise<void> {
return invoke('ai_conversation_set_pinned', { conversationId, pinned })
},
/**
/** 导出对话为指定格式(UX-18:对话导出;消费 batch46 ai_conversation_export IPC)。
/** 导出对话为指定格式对话导出;消费 batch46 ai_conversation_export IPC)。
* 后端返回导出内容 String(markdown/json/txt),前端落 Blob 下载或复制剪贴板。
* @param format 'markdown' | 'json' | 'txt'
*/
+1 -1
View File
@@ -5,7 +5,7 @@ export const ideaApi = {
/**
* 列出灵感。
*
* **双签名向后兼容**(F-260621-02):
* **双签名向后兼容**
* - `list(status?)`:旧签名,仅状态过滤(等价 `list({ status })`)。
* - `list(query?)`:新签名,多条件(status/keyword/order_by/limit/offset)走后端 WHERE。
*
+1 -1
View File
@@ -65,7 +65,7 @@ export interface ProjectQuery {
export const projectApi = {
/**
* 列出未删除项目。
* F-260621-02:可选 query(关键词/排序/分页)。不传 = 当前全量行为(向后兼容)。
* 可选 query(关键词/排序/分页)。不传 = 当前全量行为(向后兼容)。
*/
list(query?: ProjectQuery): Promise<ProjectRecord[]> {
return invoke('list_projects', { query })
+1 -1
View File
@@ -5,7 +5,7 @@ export const taskApi = {
/**
* 列出任务。
*
* **双签名向后兼容**(F-260621-02):
* **双签名向后兼容**
* - `list(projectId?)`:旧签名,仅项目过滤(等价 `list({ project_id: projectId })`)。
* - `list(query?)`:新签名,多条件(status/keyword/project_id/priority/assignee/...)走后端 WHERE。
*
+12 -12
View File
@@ -328,15 +328,15 @@ export interface AiProviderConfig {
base_url: string
default_model: string
models: string | null
/** 模型能力配置数组(F-01:4 维度 + 路由控制;DB TEXT 列 JSON 解析,老库可能缺失) */
/** 模型能力配置数组4 维度 + 路由控制;DB TEXT 列 JSON 解析,老库可能缺失) */
model_configs?: ModelConfig[]
is_default: boolean
config: string | null
created_at: string
updated_at: string
/** provider 是否进入负载均衡池(F-260614-04c)。false=仅配置存在,不参与主链路由/选池。老库默认 true。 */
/** provider 是否进入负载均衡池(c)。false=仅配置存在,不参与主链路由/选池。老库默认 true。 */
enabled?: boolean
/** provider 在负载均衡池中的选择权重(0-100,F-260614-04c)。高权重优先被选为主;同权重近似轮询。老库默认 50。 */
/** provider 在负载均衡池中的选择权重(0-100,c)。高权重优先被选为主;同权重近似轮询。老库默认 50。 */
weight?: number
}
@@ -353,7 +353,7 @@ export type Capability = 'tool_use' | 'embedding' | 'code_gen'
/** 维度 3 — 价格分级(成本)。serde snake_case 字面量。'free' 已删(B-260618-05 死档,预设/启发式从不赋值)。 */
export type CostTier = 'low' | 'medium' | 'high'
/** 维度 4 — 智力分级。serde snake_case 字面量。CR-260618-11#3:与 CostTier 同,cost/intel 已从路由硬过滤(B-260618-03)及前端标签(UX-260618-04)去除,字段保留仅 DB 兼容,不再参与路由选模型。 */
/** 维度 4 — 智力分级。serde snake_case 字面量。CR-260618-11#3:与 CostTier 同,cost/intel 已从路由硬过滤及前端标签去除,字段保留仅 DB 兼容,不再参与路由选模型。 */
export type IntelligenceTier = 'lite' | 'standard' | 'plus' | 'ultra'
/** 探测来源(只读,由后端探测器填充)。serde snake_case 字面量。 */
@@ -384,7 +384,7 @@ export interface ModelConfig {
}
/**
* AI 错误分类字面量集(B-260615-42)。
* AI 错误分类字面量集。
* 与后端 `commands::ai::ErrorType` 的 serde(rename_all = "snake_case") 严格对齐。
* 前端消费方 switch 于此常量集做差异化提示(auth 引导填 key / network 提示连接 / 等)。
*/
@@ -458,7 +458,7 @@ export type AiChatEvent = ({
} | {
type: 'AiHeartbeat'
} | {
// F-260616-03: 达 max_iterations 暂停态(后端仍 generating=true),前端展示操作卡询问继续/停止
// 达 max_iterations 暂停态(后端仍 generating=true),前端展示操作卡询问继续/停止
type: 'AiMaxRoundsReached'
} | {
// F-260619-03 Phase B: 路径授权弹窗(LLM 想访问白名单外目录,后端挂起 loop 等用户决定)。
@@ -480,7 +480,7 @@ export type AiChatEvent = ({
// enum 写入口(替代旧 bool 轨 add),消费方不再回退 streaming/generating bool 判断。
type: 'AiConvStateChanged'; conv_state: ConvState
} | {
// F-260616-07: 流式调用自动重试提示(前端可在错误气泡内显示「重试 n/m」)
// 流式调用自动重试提示(前端可在错误气泡内显示「重试 n/m」)
type: 'AiStreamRetry'; attempt: number; max_attempts: number
} | {
// F-15 上下文管理生命周期事件(原 useAiContext 独立监听器,现合入统一分发器)
@@ -674,7 +674,7 @@ export interface AiToolCallInfo {
result?: unknown
/** 审批风险说明(AiApprovalRequired 时填充) */
reason?: string
/** AE-2025-03:write_file 行级 unified diff(旧文件 vs 新内容),审批卡即时预览。
/** write_file 行级 unified diff(旧文件 vs 新内容),审批卡即时预览。
* 旧文件不存在(新建)或非 write_file 为 undefined,前端回退显新 content。 */
diff?: string
/**
@@ -729,7 +729,7 @@ export interface GoalEntry {
status: 'active' | 'completed'
}
/** 审批选择类型(F-260615-01: single=单选默认,multiple=多选) */
/** 审批选择类型single=单选默认,multiple=多选) */
export type ApprovalSelectType = 'single' | 'multiple'
/** 人工审批请求 */
@@ -739,7 +739,7 @@ export interface HumanApprovalRequest {
title: string
description: string
options: string[]
/** F-260615-01: 缺省 single(向后兼容),multiple 时允许多选 */
/** 缺省 single(向后兼容),multiple 时允许多选 */
select_type?: ApprovalSelectType
}
@@ -748,7 +748,7 @@ export interface HumanApprovalResponse {
execution_id: string
node_id: string
decision: string
/** F-260615-01: 多选结果(single 时长度=1,multiple 时长度≥1) */
/** 多选结果(single 时长度=1,multiple 时长度≥1) */
decisions?: string[]
comment?: string
}
@@ -758,7 +758,7 @@ export interface AiConversationDetail {
id: ConvId
title: string | null
messages: string // JSON string of ChatMessage[]
/** 生成中返回 true(后端 ai_conversation_switch 在 generating 时置),前端据此禁编辑(FR-C5) */
/** 生成中返回 true(后端 ai_conversation_switch 在 generating 时置),前端据此禁编辑*/
readonly?: boolean
}
+3 -3
View File
@@ -7,7 +7,7 @@ export const workflowApi = {
* 触发工作流执行,返回 execution_id。
*
* F-260616-06 阶段2(②-2): taskId/targetStatus 可选,同时传时工作流完成/失败联动推进任务。
* B-260615-34:Tauri 2 IPC 不转 camelCase,snake_case 对齐后端 workflow.rs。
* Tauri 2 IPC 不转 camelCase,snake_case 对齐后端 workflow.rs。
*/
run(
name: string,
@@ -46,8 +46,8 @@ export const workflowApi = {
* 统一以 decisions 数组表达选择,按 select_type 判定单/多选:
* - 单选:decision 取 decisions[0];
* - 多选:decisions 透传全量,decision 取首项作占位(后端按 decisions 走)。
* options 透传闭环后端校验(R-PD-5)。
* B-260615-34:Tauri 2 IPC 不转 camelCase,snake_case 对齐后端 workflow.rs。
* options 透传闭环后端校验。
* Tauri 2 IPC 不转 camelCase,snake_case 对齐后端 workflow.rs。
*/
approveHumanApproval(params: {
execution_id: string
+21 -21
View File
@@ -162,7 +162,7 @@ const router = useRouter()
const appSettings = useAppSettingsStore()
const unifiedApproval = appSettings.useSetting<boolean>('df-ai-unified-approval', true)
// ── UX-2025-20: 空状态示例问题随 MessageList 子组件迁移(examplePrompts 数组在子组件内);
// ── 空状态示例问题随 MessageList 子组件迁移(examplePrompts 数组在子组件内);
// 父仅保留 sendExamplePrompt 转发(子 emit 'send-example-prompt' → 父委托 ChatInput.sendExamplePrompt)。──
/** 跳 Settings 配置 provider:嵌入模式 router.push;分离窗口无 Settings 路由,降级 toast 提示 */
@@ -184,9 +184,9 @@ function sendExamplePrompt(i18nKey: string) {
// i18n 不在本任务白名单,文案硬编码,后续补 aiChat.toastSendFail key
const { toast, showToast } = useToast()
// B-260616-12: 工具慢执行提示事件 unlistener(onMounted 注册,onBeforeUnmount 释放)
// 工具慢执行提示事件 unlistener(onMounted 注册,onBeforeUnmount 释放)
let _unlistenToolSlow: (() => void) | null = null
// AE-2025-04: 会话信任自动放行 toast unlistener(useAiEvents 经事件总线中转,同 _unlistenToolSlow 模式)
// 会话信任自动放行 toast unlistener(useAiEvents 经事件总线中转,同 _unlistenToolSlow 模式)
let _unlistenAutoApproved: (() => void) | null = null
// P1-e-d: 审批失败 toast unlistener(useAiApproval 审批 IPC 未送达经事件总线 emit,同 _unlistenToolSlow 模式)
let _unlistenAiToast: (() => void) | null = null
@@ -206,10 +206,10 @@ function onChatInputError(payload: { msg: string; type: 'error' | 'warning' | 'i
// 第四批抽离: MessageList 子组件引用(expose scrollToBottom/scrollToFirstPending)
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)
// AE-2025-02:头部审批徽标 — 当前对话积压的待审批工具数(>0 时显示)
// 头部审批徽标 — 当前对话积压的待审批工具数(>0 时显示)
const pendingApprovalCount = computed(() => store.state.pendingApprovals.length)
// AE-2025-07: Agentic 循环进度条
// Agentic 循环进度条
// 仅 agentic 多轮(streaming 且 agentRound>0)显示;单轮问答不收 AiAgentRound 故恒隐藏。
const showAgenticProgress = computed(() =>
store.state.streaming && store.state.agentRound > 0,
@@ -236,7 +236,7 @@ async function confirmDeleteConversation(id: string) {
const conv = store.state.conversations.find(c => c.id === id)
if (!conv) console.warn('confirmDeleteConversation: conv not found, id=', id)
const title = conv?.title || t('aiChat.newConversation')
// UX-260617-24:删除当前活跃对话加醒目提示(通用文案不区分,活跃对话误删影响更大)。
// 删除当前活跃对话加醒目提示(通用文案不区分,活跃对话误删影响更大)。
const isActive = id === store.state.activeConversationId
const msgKey = isActive ? 'aiChat.confirmDeleteActiveConversation' : 'aiChat.confirmDeleteConversation'
if (!await confirmDialog(t(msgKey, { title }))) return
@@ -270,13 +270,13 @@ async function confirmNewConversation() {
// projectsStore 仍由父持有(onMounted 预加载 projects/tasks 供子组件 @ 联想读取)
const projectsStore = useProjectStore()
// UX-09:切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
// 切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
// 2026-06-18 改:打开/切换会话默认进 specify 模式并选中权重最高 model(用户诉求:默认确定选中、
// 不随机/不摇摆)。enabledModels 已按 weight 降序,[0]=最高。空池(未拉取)落 null=auto 兜底。
// 后端 run_agentic_loop 对 override 兜底校验池内才用,前端预选无副作用。
// 第三批抽离: 默认 model 选中逻辑(modelOverride=enabledModels[0])随 enabledModels 迁移至
// TopBar.vue 的 activeConversationId watch;本组件仅保留编辑态取消。
// 滚动边沿/跟随意图重置(SW-260618-18)随 MessageList 迁移,由子组件自管。
// 滚动边沿/跟随意图重置随 MessageList 迁移,由子组件自管。
watch(() => store.state.activeConversationId, () => {
if (editingMsgId.value) cancelEdit()
})
@@ -291,13 +291,13 @@ const convSidebarRef = ref<InstanceType<typeof ConversationSidebar> | null>(null
// cycleProvider/providerBarVisible/showProviderSwitched 已迁移至 ai/TopBar.vue
// (顶部工具栏 + provider/model 选择逻辑,零行为变更,store 单例共享 modelOverride)。
/** UX-09:进入编辑态 — 置 editingMsgId(透传 ChatInput prop)+ 委托 ChatInput.startEditFocus 回填/聚焦。 */
/** 进入编辑态 — 置 editingMsgId(透传 ChatInput prop)+ 委托 ChatInput.startEditFocus 回填/聚焦。 */
function startEdit(msg: AiMessage): void {
editingMsgId.value = msg.id
chatInputRef.value?.startEditFocus(msg)
}
/** UX-09:取消编辑态(切换对话/清空/ESC)。清 editingMsgId + 委托 ChatInput.clearInput 清输入。 */
/** 取消编辑态(切换对话/清空/ESC)。清 editingMsgId + 委托 ChatInput.clearInput 清输入。 */
function cancelEdit(): void {
editingMsgId.value = null
chatInputRef.value?.clearInput()
@@ -366,7 +366,7 @@ function onGlobalKeydown(e: KeyboardEvent) {
}
/**
* UX-260617-16:未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
* 未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
* 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
*/
function hasUnsentInput(): boolean {
@@ -452,15 +452,15 @@ function scrollToBottom(smooth = false) {
messageListRef.value?.scrollToBottom(smooth)
}
onBeforeUnmount(() => {
// 全局快捷键(UX-2025-07):卸载时移除 window listener,防分离窗口/切组件后泄漏
// 全局快捷键卸载时移除 window listener,防分离窗口/切组件后泄漏
window.removeEventListener('keydown', onGlobalKeydown)
// UX-260617-16:摘除未发送输入保护(分离窗口关闭/页面刷新触发 beforeunload)
// 摘除未发送输入保护(分离窗口关闭/页面刷新触发 beforeunload)
window.removeEventListener('beforeunload', onBeforeUnloadGuard)
// 注:导出菜单外部点击监听/侧栏拖拽 listener/标题淡入计时器现由 ConversationSidebar
// 子组件 onBeforeUnmount 自管(第一批抽离),父不再持有这些引用。
// 流式 rAF 清理(streaming watch 内 cancelAnimationFrame)随 MessageList 迁移,子组件
// onBeforeUnmount 自管(子 watch streaming 已注册,卸载自动停)。
// CR-260615-24: 释放后端 listener(ai-chat-event/ai-conversation-changed)+清流式看门狗。
// 释放后端 listener(ai-chat-event/ai-conversation-changed)+清流式看门狗。
// 之前 stopListener 定义但全仓零调用 → listener+watchdog 永久泄漏;分离窗口主窗口 AiChat
// 卸载(走 App.vue v-if detach)时 listener 仍挂后端,watchdog 130s 后写已卸载 state。
store.stopListener()
@@ -473,9 +473,9 @@ onBeforeUnmount(() => {
})
onMounted(async () => {
// 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
// 全局快捷键window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
window.addEventListener('keydown', onGlobalKeydown)
// UX-260617-16:未发送输入保护。AiChat 非路由组件(App.vue 内嵌 + AiDetached 路由),
// 未发送输入保护。AiChat 非路由组件(App.vue 内嵌 + AiDetached 路由),
// beforeRouteLeave 不适用。分离窗口关闭/页面刷新时若有未发送 inputText/pendingImages/
// pendingSkill,经 beforeunload 拦截提示用户。注意:e.preventDefault() 才是现代浏览器
// 触发原生「确定离开?」的标准做法 returnValue 仅 Chrome 兼容旧写法。
@@ -496,13 +496,13 @@ onMounted(async () => {
// 此处按需预加载(已加载则跳过), 不阻塞首屏(无 await, 后台执行)。
if (!projectsStore.projects.length) void projectsStore.loadProjects()
if (!projectsStore.tasks.length) void projectsStore.loadTasks()
// B-260616-12: 监听工具慢执行提示(useAiEvents 经 Tauri 事件总线广播;composable 无组件上下文,
// 监听工具慢执行提示(useAiEvents 经 Tauri 事件总线广播;composable 无组件上下文,
// 由本组件弹本地 toast。主窗口与分离窗口各挂一份 AiChat,各自消费,无重复——emit 单发,
// 两窗口的 listener 各收一次但 msg 相同,toast 各自渲染互不干扰)
_unlistenToolSlow = await listen<{ name: string }>('ai-tool-slow-toast', (e) => {
showToast(t('aiTool.executionSlow', { name: e.payload.name }), 'warning')
})
// AE-2025-04: 会话信任自动放行 toast(轻量 info,非审批气泡,避免干扰主流程)
// 会话信任自动放行 toast(轻量 info,非审批气泡,避免干扰主流程)
_unlistenAutoApproved = await listen<{ tool: string; dir: string }>('ai-tool-auto-approved-toast', (e) => {
showToast(t('aiChat.autoApprovedToast', { tool: e.payload.tool, dir: e.payload.dir }), 'info')
})
@@ -516,7 +516,7 @@ onMounted(async () => {
// 恢复生成中态(刷新后/分离窗口接管):
// - 分离模式(detached=true):resumeInDetached 接管主窗口当前对话
// - 主面板模式(detached=false):restoreGeneratingState 恢复主面板自身的生成态
// (UX-260617-13: 主面板刷新后无等价恢复链 → streaming=false + 无占位气泡 + 后端
// 主面板刷新后无等价恢复链 → streaming=false + 无占位气泡 + 后端
// 事件不进流式分支,退化为非流式。现对齐 resumeInDetached 同款恢复链)。
// 两者互斥(detached 三元决定),不会双重恢复;restoreGeneratingState 内幂等守卫(state.streaming 已 true 则跳过)。
// 在 startListener(L0 ai-client-ready 握手)之后调用,握手仍先于恢复逻辑生效。
@@ -691,7 +691,7 @@ async function initContextEventListeners(): Promise<void> {
}
/* 第五批抽离: F-260616-03 max_iterations 操作卡样式已迁移至 ai/MaxRoundsCard.vue */
/* ── AE-2025-07: Agentic 循环进度条 ── */
/* ── Agentic 循环进度条 ── */
.ai-agentic-progress {
display: flex;
align-items: center;
@@ -803,7 +803,7 @@ async function initContextEventListeners(): Promise<void> {
.ai-toast-error { background: var(--df-danger-bg); color: var(--df-danger); border: 0.5px solid var(--df-danger); }
.ai-toast-warning { background: var(--df-warning-bg); color: var(--df-warning); border: 0.5px solid var(--df-warning); }
.ai-toast-info { background: var(--df-accent-bg); color: var(--df-accent); border: 0.5px solid var(--df-accent); }
/* P0-2: ToastType 新增 'success' 兜底样式 */
/* ToastType 新增 'success' 兜底样式 */
.ai-toast-success { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid var(--df-success); }
.ai-toast-enter-active, .ai-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
.ai-toast-enter-from, .ai-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
+9 -9
View File
@@ -213,7 +213,7 @@ const isFailed = computed(() => {
if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
if (r) return true
}
// A1-B5:git 只读工具失败返 {success:false,error}(结构化非 Err),通用分支识别
// git 只读工具失败返 {success:false,error}(结构化非 Err),通用分支识别
if (r && r.success === false) return true
if (!r) {
const raw = typeof tc.result === 'string' ? tc.result : ''
@@ -223,7 +223,7 @@ const isFailed = computed(() => {
return false
})
/** AE-2025-03: write_file 审批 diff 行解析(复用 parseDiffLines 纯函数,数据源 props.tc.diff) */
/** write_file 审批 diff 行解析(复用 parseDiffLines 纯函数,数据源 props.tc.diff) */
const diffLines = computed(() => parseDiffLines(props.tc.diff))
</script>
@@ -236,16 +236,16 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
overflow: hidden;
max-width: var(--df-msg-max-width);
animation: fadeInUp 0.2s var(--df-ease);
/* UX-260617-14:相对定位锚定本卡 toast(absolute 子元素) */
/* 相对定位锚定本卡 toast(absolute 子元素) */
position: relative;
/* UX-260616-03: border/shadow/opacity 过渡让折叠/失败态切换不突兀。
/* border/shadow/opacity 过渡让折叠/失败态切换不突兀。
不做 body 高度动画(v-show display 切换 + flex gap 会留双倍间距致布局跳变,见 ToolCardList 注释)。 */
transition: border-color 0.2s var(--df-ease), box-shadow 0.2s var(--df-ease), opacity 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; }
/* UX-260616-01: 工具失败态——AR-6 下 status 仍为 completed,故单独覆盖边框与 completed 区分 */
/* 工具失败态——AR-6 下 status 仍为 completed,故单独覆盖边框与 completed 区分 */
.ai-tool-card--failed { border-color: rgba(240,101,101,0.35); box-shadow: 0 0 0 1px rgba(240,101,101,0.06); }
.ai-tool-status-dot--failed { background: var(--df-danger); }
.ai-tool-sub--failed { color: var(--df-danger); }
@@ -326,7 +326,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
border-radius: var(--df-radius-sm, 4px);
}
/* BUG-260624-02: 审批挂起时长提示(>2分钟橙色,>5分钟红色) */
/* 审批挂起时长提示(>2分钟橙色,>5分钟红色) */
.ai-tool-wait-time {
padding: 3px 10px; margin-bottom: 6px; font-size: 11px;
font-family: var(--df-font-mono);
@@ -342,7 +342,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
animation: pulseGlow 1s ease-in-out infinite;
}
/* AE-2025-03: write_file 审批 diff 预览区(红删绿增行级) */
/* write_file 审批 diff 预览区(红删绿增行级) */
.ai-tool-approval-diff { margin: 0 10px 8px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm, 4px); overflow: hidden; }
.ai-tool-diff-pre {
margin: 0; padding: 6px 0; max-height: 240px; overflow: auto;
@@ -375,7 +375,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
.ai-tool-btn--always:hover { background: var(--df-warning); color: #000; border-color: var(--df-warning); }
.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); }
/* 审批 loading(B-260616-08):禁用两按钮防重复点击,spinner 转在原位 */
/* 审批 loading禁用两按钮防重复点击,spinner 转在原位 */
.ai-tool-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.ai-tool-btn--approve:disabled:hover { background: var(--df-success-bg); color: var(--df-success); border-color: var(--df-border); }
.ai-tool-btn--reject:disabled:hover { background: var(--df-danger-bg); color: var(--df-danger); border-color: var(--df-border); }
@@ -385,7 +385,7 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
}
@keyframes aiToolBtnSpin { to { transform: rotate(360deg); } }
/* -- UX-260617-14: 审批 loading 超时兜底 toast(复用 AiChat 同款视觉 token) -- */
/* -- 审批 loading 超时兜底 toast(复用 AiChat 同款视觉 token) -- */
.ai-tool-toast {
position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
padding: 6px 12px; border-radius: var(--df-radius-sm); font-size: 11px; z-index: 100;
+14 -14
View File
@@ -81,12 +81,12 @@ const emit = defineEmits<{
const rootEl = ref<HTMLElement>()
// 卡片级折叠(整卡 body 显隐,区别于 read_file 内容级)
const expandedCards = ref(new Set<string>())
// UX-260616-03:用户主动展开的卡片(记忆态,不被自动收起清除)。
// 用户主动展开的卡片(记忆态,不被自动收起清除)。
// 与 expandedCards 区分:expandedCards 是"展开"语义的当前态(含默认首卡),
// userExpandedCards 是"用户明确点开"的持久标记,collapseInactive 跳过这些卡。
// 语义:用户读过的卡不强制折叠,符合"跟随阅读位置"的增强目标。
const userExpandedCards = ref(new Set<string>())
// UX-260617-23:用户主动折叠记忆(修复首卡 ci===0 兜底 return true 导致无法折叠)。
// 用户主动折叠记忆(修复首卡 ci===0 兜底 return true 导致无法折叠)。
// isCardExpanded 最优先判断,覆盖默认首卡展开 + userExpandedCards 记忆。
const userCollapsedCards = ref(new Set<string>())
// 内容级展开(read_file "展开全部")
@@ -125,7 +125,7 @@ const collapsibleGroupCount = computed(() =>
groupedToolCalls.value.filter(g => g.calls.length >= 2).length
)
/** 首个可折叠分组索引(U-260618:全部展开/收起按钮下沉到此分组标题行尾巴) */
/** 首个可折叠分组索引全部展开/收起按钮下沉到此分组标题行尾巴) */
const firstCollapsibleGi = computed(() => {
for (let i = 0; i < groupedToolCalls.value.length; i++) {
if (groupedToolCalls.value[i].calls.length >= 2) return i
@@ -196,7 +196,7 @@ function toggleAllGroups(): void {
/**
* 分组显示名:工具名 → i18n 友好标签(英文化,原硬编码中文)。
* UX-260617-17:用 aiTool.toolGroup.* 子键查表;未命中的工具回退 _ → 空格风格。
* 用 aiTool.toolGroup.* 子键查表;未命中的工具回退 _ → 空格风格。
*/
// tool.name → aiTool.toolGroup.* 子键(与后端 tool_registry 工具名一一对应)
const TOOL_GROUP_I18N_KEY: Record<string, string> = {
@@ -248,14 +248,14 @@ function groupFileSummary(group: ToolCallGroup): string {
* - 分组未折叠 → 用户手动 expandedCards 或首卡默认展开
* - 分组已折叠 → false(整组隐藏)
* - 多卡片分组 + 未手动操作过 → 仅首卡(ci===0)展开
* - UX-260616-03:userExpandedCards(用户主动展开记忆)优先,即使父级 collapseInactive
* - userExpandedCards(用户主动展开记忆)优先,即使父级 collapseInactive
* 清了 expandedCards,记忆态仍保持展开(用户读过的卡不强制折叠)。
*/
function isCardExpanded(tc: AiToolCallInfo, gi: number, ci: number): boolean {
if (isGroupCollapsed(gi)) return false
// UX-260617-23:用户主动折叠最优先(覆盖默认首卡 ci===0 兜底展开 + 用户展开记忆)
// 用户主动折叠最优先(覆盖默认首卡 ci===0 兜底展开 + 用户展开记忆)
if (userCollapsedCards.value.has(tc.id)) return false
// UX-260616-03:用户主动展开的卡片记忆态优先(不被自动收起影响)
// 用户主动展开的卡片记忆态优先(不被自动收起影响)
if (userExpandedCards.value.has(tc.id)) return true
// 用户手动切换过该卡(当前态)
if (expandedCards.value.has(tc.id)) return true
@@ -290,19 +290,19 @@ function toggleExpand(id: string) {
/**
* 收起不在 activeIds 中的已完成/已拒绝卡片(供父组件 auto-collapse watch 调用)。
* UX-260616-03:userExpandedCards(用户主动展开记忆)不被清除——用户读过的卡
* userExpandedCards(用户主动展开记忆)不被清除——用户读过的卡
* 即使不再 active(running/pending/write_file)也保持展开,避免"我刚才在看,突然被收起"。
* 仅清 expandedCards(默认态/首卡)与 expandedTools(内容级展开,跟随卡片折叠)。
*/
function collapseInactive(activeIds: Set<string>): void {
// CR-260615-31:对称过滤 expandedTools(原只清 expandedCards,expandedTools 残留致折叠后内容展开态)
// 对称过滤 expandedTools(原只清 expandedCards,expandedTools 残留致折叠后内容展开态)
if (expandedCards.value.size > 0) {
expandedCards.value = new Set([...expandedCards.value].filter(id => activeIds.has(id)))
}
if (expandedTools.value.size > 0) {
expandedTools.value = new Set([...expandedTools.value].filter(id => activeIds.has(id)))
}
// UX-260616-03:不清 userExpandedCards(记忆态),isCardExpanded 优先读它保持展开。
// 不清 userExpandedCards(记忆态),isCardExpanded 优先读它保持展开。
}
/**
@@ -377,7 +377,7 @@ function groupIcon(name: string): string {
gap: 8px;
}
/* -- 顶部操作栏:批量审批 + 全局收起合并同行(UX-260616-02) -- */
/* -- 顶部操作栏:批量审批 + 全局收起合并同行-- */
.ai-tool-topbar {
display: flex;
align-items: center;
@@ -419,7 +419,7 @@ function groupIcon(name: string): string {
}
.ai-btn--danger:hover { filter: brightness(1.1); }
/* -- 全部展开/收起(U-260618:从 topbar 下沉到分组标题行,移除独立按钮 padding/背景,
/* -- 全部展开/收起从 topbar 下沉到分组标题行,移除独立按钮 padding/背景,
高度=文字行高,不撑高分组标题行;融入标题视觉,仅 hover 变色) -- */
.ai-tool-global-toggle {
display: flex;
@@ -447,7 +447,7 @@ function groupIcon(name: string): string {
align-items: center;
gap: 6px;
padding: 6px 10px;
/* U-260618: max-width 对齐气泡/卡片(90%),不拉满 ai-msg-content 右侧 */
/* max-width 对齐气泡/卡片(90%),不拉满 ai-msg-content 右侧 */
max-width: var(--df-msg-max-width);
cursor: pointer;
user-select: none;
@@ -483,7 +483,7 @@ function groupIcon(name: string): string {
color: var(--df-accent);
}
.ai-tool-group-chevron {
/* U-260618: 移除 margin-left:auto,让全部展开/收起按钮(global-toggle)占据行尾右对齐 */
/* 移除 margin-left:auto,让全部展开/收起按钮(global-toggle)占据行尾右对齐 */
font-size: 10px;
color: var(--df-text-dim);
transition: transform 0.15s var(--df-ease);
+5 -5
View File
@@ -277,7 +277,7 @@ const isFailed = computed(() => {
if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
if (r) return true
}
// A1-B5: 可解析 JSON 且显式 success===false(git status/diff/log 只读工具失败返
// 可解析 JSON 且显式 success===false(git status/diff/log 只读工具失败返
// {success:false,error} 合法 JSON 对象非 Err)→ 判失败,防绿框假成功
if (r && r.success === false) return true
if (!r) {
@@ -296,13 +296,13 @@ const cmdOutput = computed(() => {
})
/**
* UX-260618-06: patch_file 结果 diff 行拆解(复用 parseDiffLines +add/-del/ ctx 着色)。
* patch_file 结果 diff 行拆解(复用 parseDiffLines +add/-del/ ctx 着色)。
* 数据源:parsed.diff(patch_file 工具执行结果字段)。长 diff 截断到前 120 行防撑爆卡片。
*/
const resultDiffLines = computed(() => parseDiffLines(parsed.value?.diff, 120))
/**
* grep 命中计数标签(F-260621):G-1 复用 useToolCard.formatGrep 单一函数,
* grep 命中计数标签G-1 复用 useToolCard.formatGrep 单一函数,
* 消除原与 ToolResultBody 重复的三分支(files_with_matches/count/content 文案)。
* parsed 为 null 返空串,等价原逻辑;非 null 委托 formatGrep。
*/
@@ -333,7 +333,7 @@ function onInstallObscura() {
emit('install-obscura')
}
// SW-260618-06: status 切到 completed 时按成功/失败初始化折叠态(失败默认展开便于看 stderr/错误 body)。
// status 切到 completed 时按成功/失败初始化折叠态(失败默认展开便于看 stderr/错误 body)。
// immediate 时若初始非 completed,两 ref 保持默认 false,无副作用。
watch(() => props.tc.status, (s) => {
if (s === 'completed') {
@@ -402,7 +402,7 @@ watch(() => props.tc.status, (s) => {
color: var(--df-warning);
}
/* grep 结果(F-260621):文件栏复用 ai-tool-file-bar,命中行用等宽渲染 */
/* grep 结果文件栏复用 ai-tool-file-bar,命中行用等宽渲染 */
.ai-tool-grep-result {
border-top: 0.5px solid var(--df-border);
}
+6 -6
View File
@@ -128,7 +128,7 @@ import type { AiMessage, ContentPart, SkillInfo, ProjectRecord, TaskRecord, Idea
import type { MentionItem } from './MentionItem'
const props = defineProps<{
/** UX-09: 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
/** 编辑末条 user 消息态;父持有,handleSend 据此走 editMessage 而非 sendMessage */
editingMsgId: string | null
}>()
@@ -317,7 +317,7 @@ watch(inputText, (v) => {
// / 技能联想(仅当输入以 / 开头且光标在首字符区)
skillOpen.value = v.startsWith('/')
if (skillOpen.value) skillIndex.value = 0
// UX-10: @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开;视觉打架防护)
// @ 实体引用(技能联想与 @ 联想互斥,二者同时仅一开;视觉打架防护)
if (skillOpen.value) {
mentionOpen.value = false
mentionStart.value = -1
@@ -327,7 +327,7 @@ watch(inputText, (v) => {
})
/**
* UX-10: 检测光标前最近的 @ 触发符。
* 检测光标前最近的 @ 触发符。
* 规则:从光标位置向前找最近的 @,期间无空白/换行则激活(支持 "abc @dev" 但不支持 "a @b c @d" 中第二个被空格阻断)。
* 找到 → mentionStart=该 @ 下标, mentionOpen=true; 否则关闭。
*/
@@ -675,7 +675,7 @@ function selectMention(item: MentionItem) {
}
function handleKeydown(e: KeyboardEvent) {
// UX-09:编辑态 Escape 取消编辑(最高优先级)
// 编辑态 Escape 取消编辑(最高优先级)
if (props.editingMsgId && e.key === 'Escape') {
e.preventDefault()
emit('cancel-edit')
@@ -716,7 +716,7 @@ function handleKeydown(e: KeyboardEvent) {
return
}
}
// UX-10: @ 实体引用浮层导航(仅有匹配项时拦截方向/回车;与技能联想互斥)
// @ 实体引用浮层导航(仅有匹配项时拦截方向/回车;与技能联想互斥)
if (mentionOpen.value && mentionItems.value.length) {
if (e.key === 'ArrowDown') {
e.preventDefault()
@@ -907,7 +907,7 @@ function focus(): void {
}
/**
* UX-260617-16:未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
* 未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
* 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
*/
function hasUnsentInput(): boolean {
+7 -7
View File
@@ -322,7 +322,7 @@ function onActionsOutsideClick(e: MouseEvent) {
}
/**
* UX-260617-23:搜索结果点击切对话后清空 searchQuery,退出搜索模式回原时间分组视图。
* 搜索结果点击切对话后清空 searchQuery,退出搜索模式回原时间分组视图。
* M3:命中项可能在折叠的时间分组或折叠的归档分组内,选中态不可见。切回分组视图后展开所在分组
* 并 scrollIntoView,确保选中项立即可见(原仅切 searchQuery,选中项落在折叠组里看不到高亮)。
*/
@@ -425,7 +425,7 @@ const searchResults = computed<AiConversationSummary[] | null>(
() => store.filteredConversations.value,
)
// ── UX-2025-16: 侧栏宽度拖拽 ──
// ── 侧栏宽度拖拽 ──
const resizeState = { startX: 0, startWidth: 0 }
function onResizeMove(e: MouseEvent) {
store.setSidebarWidth(resizeState.startWidth + (e.clientX - resizeState.startX))
@@ -451,11 +451,11 @@ onMounted(() => {
onBeforeUnmount(() => {
document.removeEventListener('click', onActionsOutsideClick)
// UX-2025-16:拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移)
// 拖拽中途卸载兜底摘临时 document listener(原 AiChat 同段迁移)
document.removeEventListener('mousemove', onResizeMove)
document.removeEventListener('mouseup', onResizeEnd)
document.body.classList.remove('ai-sidebar-resizing')
// UX-2025-20:释放标题淡入计时器
// 释放标题淡入计时器
if (_titleFlashTimer) { clearTimeout(_titleFlashTimer); _titleFlashTimer = null }
})
@@ -482,7 +482,7 @@ defineExpose({
height: 100%;
background: var(--df-bg);
}
/* UX-2025-16:右边缘 2px 拖拽条。hover/active 显高亮;拖拽中 body.ai-sidebar-resizing 全局锁 cursor 防抖 */
/* 右边缘 2px 拖拽条。hover/active 显高亮;拖拽中 body.ai-sidebar-resizing 全局锁 cursor 防抖 */
.ai-sidebar-resize-handle {
position: absolute;
top: 0;
@@ -662,7 +662,7 @@ body.ai-sidebar-resizing * {
font-size: 9px;
color: var(--df-text-dim);
}
/* F-09: 多会话并发生成指示器(会话项标题行内脉冲点,多会话可同时显示) */
/* 多会话并发生成指示器(会话项标题行内脉冲点,多会话可同时显示) */
.ai-conv-gen-dot {
display: inline-block;
width: 6px;
@@ -706,7 +706,7 @@ body.ai-sidebar-resizing * {
background: var(--df-sidebar-hover);
color: var(--df-text);
}
/* CR-22-1: 已置顶对话视觉强调(左边线,不依赖菜单展开,一眼识别)。
/* 已置顶对话视觉强调(左边线,不依赖菜单展开,一眼识别)。
pinned 类挂在 .ai-conv-item 上(操作收进更多菜单后,按钮不再常驻,改 item 级类)。 */
.ai-conv-item--pinned {
box-shadow: inset 2px 0 0 var(--df-accent, var(--df-text));
+1 -1
View File
@@ -70,7 +70,7 @@ const { confirmState, confirmDialog, answerConfirm } = useConfirm()
// 首轮慢响应(看门狗已超时清 streaming)之后——此时 streaming=false 但后端 generating=true、
// generatingConvs 含 conv(useAiEvents handleEvent 对非完成/错误事件 add)。若守卫仍 && streaming,
// 挂起弹窗永不显示 → loop 无声卡死(用户发消息无响应)。去 streaming,isGenerating(conv) 单条件覆盖。
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
// 多会话并发 — isGenerating(active) 替代单值比对
const isViewingGenerating = computed(() =>
store.isGenerating(store.state.activeConversationId),
)
+2 -2
View File
@@ -20,7 +20,7 @@ const emit = defineEmits<{
(e: 'go-to-settings'): void
}>()
// ── UX-2025-20: 空状态示例问题 ──
// ── 空状态示例问题 ──
// 示例问题卡片(点击自动填入并发送);文案走 i18n key 数组,模板按 key 翻译。
const examplePrompts = [
'aiChat.examplePrompt1',
@@ -85,7 +85,7 @@ const examplePrompts = [
text-align: center;
line-height: 1.5;
}
/* UX-2025-20: 无 provider 引导 — 跳 Settings 按钮(克制:边框线 + accent 文字,不喧宾) */
/* 无 provider 引导 — 跳 Settings 按钮(克制:边框线 + accent 文字,不喧宾) */
.ai-empty-action {
display: inline-flex;
align-items: center;
+1 -1
View File
@@ -43,7 +43,7 @@ const { t } = useI18n()
// 耗时长,130s 流式看门狗极易在迭代间隙超时清 streaming → 卡片因 streaming=false 不弹 → 用户看不到
// "继续/停止" → generating 永真卡死。达 max 比 DirAuthAuth 更易触发(必经多轮)。去 streaming,
// isGenerating(conv) 单条件覆盖挂起态(达 max 时 generatingConvs 含 conv,useAiEvents handleEvent 对非完成/错误事件 add)。
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
// 多会话并发 — isGenerating(active) 替代单值比对
const isViewingGenerating = computed(() =>
store.isGenerating(store.state.activeConversationId),
)
+30 -30
View File
@@ -28,7 +28,7 @@ import { formatRelative, formatDate } from '../../utils/time'
import type { AiMessage } from '../../api/types'
defineProps<{
/** UX-09:当前正在编辑的消息 id(父持有,经 ChatInput 透传);末条 user 编辑按钮据此隐藏 */
/** 当前正在编辑的消息 id(父持有,经 ChatInput 透传);末条 user 编辑按钮据此隐藏 */
editingMsgId?: string | null
}>()
@@ -42,8 +42,8 @@ const emit = defineEmits<{
const store = useAiStore()
const { t } = useI18n()
// ═══ Markdown 渲染(ARC-260615-08:自研块级 memo,借鉴方案D流式核心) ═══
// B-23:渲染基础设施(_marked/_purify/mdReady/_mdCache/loadMarkdown/escapeFallback/
// ═══ Markdown 渲染自研块级 memo,借鉴方案D流式核心) ═══
// 渲染基础设施(_marked/_purify/mdReady/_mdCache/loadMarkdown/escapeFallback/
// renderMd)抽至 composables/useMarkdown(模块级单例),与 TaskDetail/AiChat 共享。
// 流式核心(splitBlocks/parseBlock/renderStreamingBlocks/scheduleStreamParse)抽至
// composables/ai/useStreamRenderer(本组件经 opts 注入 saveSelection/restoreSelection/
@@ -54,14 +54,14 @@ const { t } = useI18n()
// scheduleStreamParse/renderStreamingBlocks/clearStreamingState 均由 composable 返回。
const { loadMarkdown } = useMarkdown()
// ── UX-2025-20: 空状态示例问题(examplePrompts 已随 EmptyState.vue 提取) ──
// ── 空状态示例问题(examplePrompts 已随 EmptyState.vue 提取) ──
const messagesContainer = ref<HTMLDivElement>()
// 工具卡片列表(子组件 ToolCardList 自治折叠态,父级仅经 expose 调 collapseInactive 收起旧卡)
const toolCardListRef = ref<InstanceType<typeof ToolCardList>>()
// 当前视图是否正在生成(切走后台生成时光标不显示)
// F-09: 多会话并发 — isGenerating(active) 替代单值比对
// 多会话并发 — isGenerating(active) 替代单值比对
const isViewingGenerating = computed(() =>
store.state.streaming && store.isGenerating(store.state.activeConversationId)
)
@@ -72,10 +72,10 @@ function isLastAi(msg: AiMessage): boolean {
}
/**
* B-260618-19: 是否渲染某条消息(供 .ai-msg-slot v-if 守卫)。
* 是否渲染某条消息(供 .ai-msg-slot v-if 守卫)。
*
* 背景:agentic 多轮下,LLM 某轮只发 tool_calls 不输出文本时(GLM anthropic_compat 常见),
* 该轮 assistant 消息 content='' + toolCalls=[...];变中间条后,旧 v-else-if(UX-260617-22)
* 该轮 assistant 消息 content='' + toolCalls=[...];变中间条后,旧 v-else-if
* 漏判 toolCalls 把它整条过滤 → 工具卡片消失,且外层 .ai-msg-slot 恒渲染留空 div,
* flex gap 堆积成大片空白,末条光标气泡随每轮 AiAgentRound push 往下移。
*
@@ -207,14 +207,14 @@ async function handleMessagesScroll(): Promise<void> {
}
watch(() => store.state.messages.length, onContentChange)
// SW-260618-07: currentText → onContentChange 合并到下方 scheduleStreamParse watch(同源 currentText,单 callback 顺序执行)
// currentText → onContentChange 合并到下方 scheduleStreamParse watch(同源 currentText,单 callback 顺序执行)
// 流式 Markdown 渲染驱动(ARC-260615-08 块级 memo):
// - currentText 变化(每 delta):rAF 节流 scheduleStreamParse,块级 memo 只重 parse 末块
// - streaming 翻转:结束清 rAF + 清 streamingBlocks(回 renderMd 走 final marked + 缓存)
// - mdReady 翻转(CR-260615-06):首屏 marked 慢 + 暂无新 delta 时末段停留 escapeFallback
// - mdReady 翻转首屏 marked 慢 + 暂无新 delta 时末段停留 escapeFallback
// 纯文本;marked 就绪后主动重算一次,使纯文本转格式化
// SW-260618-07: 合并 currentText 双 watch(原 :2040 onContentChange 滚动 + 本 watch scheduleStreamParse 分块)。
// 合并 currentText 双 watch(原 :2040 onContentChange 滚动 + 本 watch scheduleStreamParse 分块)。
// 顺序:onContentChange(滚动跟随)先 → scheduleStreamParse(rAF 分块渲染)后,保持原注册顺序。
watch(() => store.state.currentText, (text) => {
// 流式滚动跟随由 scheduleStreamParse 的 rAF 回调承接(DOM 高度就绪后滚),
@@ -244,7 +244,7 @@ watch(() => store.state.streaming, (s) => {
// streamingBlocks(依赖 currentText watch 的 !text 分支按新轮时机清,BUG-260624-01)。
cancelPendingRaf()
if (s) {
// UX-260616-03: 新一轮生成开始 → 收起上一轮已完成的旧工具卡,聚焦当前生成内容。
// 新一轮生成开始 → 收起上一轮已完成的旧工具卡,聚焦当前生成内容。
// 仅当前视图正在生成时触发(isGenerating(activeConversationId)),
// 切走后台生成/恢复会话(streaming 初值已 true 的 onMounted 路径)不误触。
// buildActiveToolIds/collapseAllToolLists 是 function 声明,提升可用。
@@ -252,7 +252,7 @@ watch(() => store.state.streaming, (s) => {
collapseAllToolLists(buildActiveToolIds(store.state.messages))
}
} else {
// UX-2025-01: 流式→完成切换,模板从 v-for 分块切到 v-html 整段,DOM 重建
// 流式→完成切换,模板从 v-for 分块切到 v-html 整段,DOM 重建
saveSelection()
clearStreamingState()
nextTick(() => restoreSelection())
@@ -270,7 +270,7 @@ watch(() => store.state.streaming, (s) => {
// 只在 toolCall 状态翻转(新卡片/完成/拒绝)时触发,与折叠语义精确对齐。
// snap/MESSAGE_CAP 兜底机制保持不变(deep watch 仍做变化检测,只是 body 短路掉无意义重算)。
//
// UX-260616-03: 触发时机扩展。除原 messages deep watch(toolCall 状态翻转/新消息条数),
// 触发时机扩展。除原 messages deep watch(toolCall 状态翻转/新消息条数),
// 另加 agentRound watch(AiAgentRound 新轮次 → 收起上一轮工具卡,聚焦当前轮)。
// active Set 推导逻辑复用 buildActiveToolIds,两 watch 共用。
function buildActiveToolIds(msgs: typeof store.state.messages): Set<string> {
@@ -330,7 +330,7 @@ watch(
},
)
// UX-260616-03: AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
// AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
// 首次加载不触发(等真到达更"新"的轮次才收起,避免历史会话加载即折叠)。
watch(
() => store.state.agentRound,
@@ -340,12 +340,12 @@ watch(
},
)
// CR-260615-06: mdReady 就绪后主动触发首屏 Markdown 重渲染
// mdReady 就绪后主动触发首屏 Markdown 重渲染
// (marked 加载慢 + 无新 delta 时末段停留纯文本,不触发模板重算)
const _mdRenderKey = ref(0)
function forceUpdate() { _mdRenderKey.value++ }
// ── UX-2025-01: 选文字保持(save/restore Selection) ──
// ── 选文字保持(save/restore Selection) ──
// v-html 整体替换 innerHTML 会销毁子 DOM 节点 → Node 引用断连 → 选区丢失。
// 方案: 保存为「气泡内文本偏移量 + 选中文字」,重建后按偏移量重新定位。
// 偏移量对 Markdown 渲染结果稳定(同内容→同DOM文本节点序列→同偏移)。
@@ -484,7 +484,7 @@ watch(mdReady, (ready) => {
}
})
// ── 消息气泡拷贝(F-260615-13) ──
// ── 消息气泡拷贝──
// 必须在 click 事件内(或其同步调用链)调 clipboard API (浏览器安全限制)
// navigator.clipboard 在非 secure context (Electron file:// / 旧版 webview) 不可用,
// 回退到 execCommand('copy') 兜底;两者均失败时提示复制失败而非假成功。
@@ -558,7 +558,7 @@ async function copyMsgAsImage(msg: AiMessage): Promise<void> {
}
}
// ── UX-03: 错误气泡操作入口 ──
// ── 错误气泡操作入口 ──
// errorType 经 useAiEvents push 错误消息时附带(types.ts AiMessage 未含此字段,
// 经 cast 读写闭环在 AiChat.vue / useAiEvents 两端,不污染 types.ts 定义)。
interface AiMessageWithErrorType extends AiMessage {
@@ -572,7 +572,7 @@ function canOpenSettings(msg: AiMessage): boolean {
}
/**
* UX-260616-01: 错误是否可重试(显隐「重试」按钮)。
* 错误是否可重试(显隐「重试」按钮)。
* Fatal 类错误(鉴权/Provider 配置错——4xx/401/403/invalid_api_key)重试必再败,
* 隐藏重试按钮,引导用户走「去设置」。network/timeout/unknown 为瞬时错误,可重试。
* retryable 推断依据后端 error_type(本批前端推断;后端 AiStreamRetry/AiError 加 retryable 字段下批 df-ai 增强)。
@@ -587,7 +587,7 @@ function canRetry(msg: AiMessage): boolean {
/**
* 错误气泡「重试」:取错误前的末尾 user 消息重发。
* 复用 regenerate 链路(后端弹末尾 AI 回复——错误消息本身——取触发它的 user 消息重跑)。
* 语义等价"重新生成",但语义场景是错误恢复,文案/入口独立(UX-03)。
* 语义等价"重新生成",但语义场景是错误恢复,文案/入口独立。
*/
async function handleRetry(): Promise<void> {
try {
@@ -741,9 +741,9 @@ const renderItems = computed<RenderItem[]>(() => {
return items
})
// UX-09:切换对话/新对话时由父取消编辑态;滚动边沿/跟随意图重置在此自管(SW-260618-18)。
// 切换对话/新对话时由父取消编辑态;滚动边沿/跟随意图重置在此自管。
watch(() => store.state.activeConversationId, () => {
// SW-260618-18: 切会话重置滚动边沿检测 + 跟随意图(新会话默认跟随底部)
// 切会话重置滚动边沿检测 + 跟随意图(新会话默认跟随底部)
setFollowing(true)
// BUG-260624-01(消息重叠根治·论证 workflow 完整性高优):MessageList 是持久单实例(AiChat.vue
// 无 :key/v-if 卸载),实例级 streamingBlocks(:69)跨会话/跨轮残留是消息重叠主根因之一。
@@ -1043,7 +1043,7 @@ defineExpose({
flex-direction: column;
gap: 14px;
}
/* UX-2025-19: .ai-messages 的直接 flex 子(B-260618-01:虚拟滚动 sentinel 已移除,
/* .ai-messages 的直接 flex 子虚拟滚动 sentinel 已移除,
现仅作消息分组的语义容器,gap:14px 由 .ai-messages 承担)。 */
.ai-msg-slot { display: flex; flex-direction: column; }
@@ -1148,7 +1148,7 @@ defineExpose({
color: var(--df-danger);
border-color: rgba(240,101,101,0.3);
}
/* B-260618-06:修正 UX-260617-21 的 display:block 方案——display:block 让 table 失去表格
/* 修正 UX-260617-21 的 display:block 方案——display:block 让 table 失去表格
布局上下文,tr/td 脱离 table 上下文塌成块级堆叠丢列对齐(用户截图反馈测试报告四列表格错乱)。
正解:table 保持默认 display:table(列对齐恢复)+ max-width:100%;宽表格横向滚动交由气泡
容器 .ai-msg-bubble--ai.ai-md 的 overflow-x:auto 承载(CSS spec 中 table-display 下
@@ -1253,7 +1253,7 @@ defineExpose({
.ai-msg-chip--idea { background: rgba(255, 255, 255, 0.22); border: 1px solid rgba(255, 255, 255, 0.35); }
.ai-msg-chip--skill { background: rgba(0, 0, 0, 0.18); }
/* ── 消息时间戳(UX-2025-13) ── */
/* ── 消息时间戳── */
.ai-msg-time {
font-size: 10px;
line-height: 1;
@@ -1268,7 +1268,7 @@ defineExpose({
align-self: flex-end; /* 用户消息:靠气泡右对齐 */
}
/* ── 消息气泡拷贝按钮(F-260615-13) ── */
/* ── 消息气泡拷贝按钮── */
.ai-copy-btn {
position: absolute;
right: 4px;
@@ -1299,7 +1299,7 @@ defineExpose({
right: 30px;
}
/* ── UX-09: 末条 user 消息编辑按钮(hover 显示,回填输入框编辑重生成) ── */
/* ── 末条 user 消息编辑按钮(hover 显示,回填输入框编辑重生成) ── */
.ai-msg-edit-btn {
display: flex;
align-items: center;
@@ -1324,7 +1324,7 @@ defineExpose({
color: var(--df-text);
}
/* ── UX-02/UX-03: 消息操作栏(hover 显示 / 错误气泡常显) ── */
/* ── UX-02/消息操作栏(hover 显示 / 错误气泡常显) ── */
.ai-msg-actions {
display: flex;
align-items: center;
@@ -1338,7 +1338,7 @@ defineExpose({
.ai-msg-actions--error {
opacity: 1;
}
/* UX-260616-01: Fatal 错误(不可重试)的提示文案——替代隐藏的「重试」按钮,
/* Fatal 错误(不可重试)的提示文案——替代隐藏的「重试」按钮,
说明为何无重试入口,引导走「去设置」。 */
.ai-msg-not-retryable {
font-size: 10.5px;
@@ -1492,7 +1492,7 @@ defineExpose({
background: var(--df-bg);
}
/* ═══ F-15: 压缩摘要 system 块(compressed 原文已展开,摘要置顶提示发给 AI 的是它) ═══
/* ═══ 压缩摘要 system 块(compressed 原文已展开,摘要置顶提示发给 AI 的是它) ═══
用户诉求:压缩只后端省 token,界面不藏消息。原文 compressed 照常 user/ai 气泡展开,
摘要 system 块置顶 accent 提示,让用户知情"AI 看到的是摘要"。 */
.ai-msg-system {
+8 -8
View File
@@ -334,7 +334,7 @@ watch(() => store.state.activeConversationId, () => {
}
})
// 切 provider 后模型 override 同步(BUG-2026-07-07:provider 切换后 header 模型名不刷新):
// 切 provider 后模型 override 同步provider 切换后 header 模型名不刷新):
// - modelOverride 是模块级 ref(useAiSend)不持久化,切 provider 后仍持旧 provider 的 model_id,
// 该 id 不在新 provider 池里 → 下拉显示错位(UI 显示 X,实际后端用路由默认 Y)。
// - 守卫:当前选择对新 provider 模型池无效(空/不在池)时,默认选权重最高模型。
@@ -400,7 +400,7 @@ const historyExpanded = ref(false)
function scrollToMessage(msgId: string) {
historyExpanded.value = false
// 消息列表已有 data-msg-id 属性,直接 DOM 查询定位。
// BUG-260802:msgId 由后端生成,可能含 CSS 选择器元字符(: . [ ] / 空格 等),
// msgId 由后端生成,可能含 CSS 选择器元字符(: . [ ] / 空格 等),
// 模板拼 selector 会被选择器解析器当伪类/类名异常 → querySelector 抛错或返回 null。
// 用 CSS.escape 转义 msgId,确保任意字符串安全嵌入属性选择器。
// 旧浏览器无 CSS.escape 的兜底:encodeURIComponent + attr*= 模糊匹配(非精确,降级路径)。
@@ -542,18 +542,18 @@ async function removeGoal(i: number) {
function cycleProvider() {
const ps = store.state.providers
if (ps.length < 2) return
// F-260616-10: activeProvider 为 null 时从 DB 默认(is_default)起算,避免 findIndex=-1 跳到 providers[0]
// activeProvider 为 null 时从 DB 默认(is_default)起算,避免 findIndex=-1 跳到 providers[0]
const startId = store.state.activeProvider || ps.find(p => p.is_default)?.id || null
const idx = startId ? ps.findIndex(p => p.id === startId) : -1
// idx=-1 且无默认 provider 时 fallback 到 0(极端边界,正常路径不会走到)
const next = ps[(Math.max(idx, 0) + 1) % ps.length]
store.setProvider(next.id)
// UX-2025-14: 切换反馈 — toast 提示当前 model + 临时 bar 展示 model 名(2s 后淡出)。
// 切换反馈 — toast 提示当前 model + 临时 bar 展示 model 名(2s 后淡出)。
// toast 机制:复用父自管 reactive showToast(分离窗口无 App.vue 根 toast),经 emit 回父弹。
showProviderSwitched(next.name || next.id)
}
// ── UX-2025-14: Provider 切换临时 bar ──
// ── Provider 切换临时 bar ──
// 切换后展示当前 model 名小字,0.15s 淡入,2s 后淡出。本子组件自管 bar 显隐(ref + Transition),
// toast 经 emit 回父(父持有 toast reactive 容器)。
const providerBarVisible = ref(false)
@@ -567,7 +567,7 @@ function showProviderSwitched(modelName: string) {
_providerBarTimer = setTimeout(() => { providerBarVisible.value = false }, 2000)
}
// UX-2025-14:释放 provider 切换 bar 计时器(从 AiChat onBeforeUnmount 迁移,与 _providerBarTimer 同处)
// 释放 provider 切换 bar 计时器(从 AiChat onBeforeUnmount 迁移,与 _providerBarTimer 同处)
onBeforeUnmount(() => {
if (_providerBarTimer) { clearTimeout(_providerBarTimer); _providerBarTimer = null }
})
@@ -604,7 +604,7 @@ onBeforeUnmount(() => {
white-space: nowrap;
max-width: 200px;
}
/* AE-2025-02:审批徽标(待审批>0 时显于头部,点击跳首个审批卡) */
/* 审批徽标(待审批>0 时显于头部,点击跳首个审批卡) */
.ai-approval-badge {
display: inline-flex;
align-items: center;
@@ -1014,7 +1014,7 @@ onBeforeUnmount(() => {
font-size: 11px;
color: var(--df-danger);
}
/* UX-2025-14: Provider 切换临时 bar(淡入淡出 0.15s) */
/* Provider 切换临时 bar(淡入淡出 0.15s) */
.ai-provider-switch-bar {
display: flex;
align-items: center;
@@ -114,7 +114,7 @@ const displayProjects = computed(() =>
.dot-release { background: var(--df-success); }
.dot-planning { background: var(--df-info); }
.dot-cancelled { background: var(--df-text-dim); }
/* WK-1:paused 置灰(对齐 cancelled dim 档,临时暂停不误标为已取消) */
/* paused 置灰(对齐 cancelled dim 档,临时暂停不误标为已取消) */
.dot-paused { background: var(--df-text-dim); }
.project-name {
+8 -8
View File
@@ -322,7 +322,7 @@ const emit = defineEmits<{
(e: 'promote'): void
(e: 'delete'): void
(e: 'status-change', value: IdeaStatus): void
// WK-16:返 Promise<boolean> 让子组件 await 知成功/失败。
// 返 Promise<boolean> 让子组件 await 知成功/失败。
// true=落库成功(关编辑态);false=失败(保持编辑态,toast 由父组件处理)。
(e: 'update-desc', value: string): Promise<boolean>
(e: 'update-related', ids: string[]): Promise<void>
@@ -344,7 +344,7 @@ function startEdit() {
}
// P1-⑥ saveEdit 改异步:emit 返回 Promise<boolean>(父组件 store.updateIdea 落库结果),
// 仅成功时关编辑态;失败保持编辑态 + toast 由父组件处理(WK-16)。
// 仅成功时关编辑态;失败保持编辑态 + toast 由父组件处理。
async function saveEdit() {
if (savingDesc.value) return
savingDesc.value = true
@@ -392,7 +392,7 @@ watch(() => props.idea.description, () => void nextTick(measureDesc))
// P1-④ 状态切换 select → badge 点击:点击当前 badge 展开/收起下拉菜单,
// 选中项后 emit('status-change') 交父组件确认+落库(沿用既有契约,不破坏 statusOptions 来源)。
const statusMenuOpen = ref(false)
// WK-5:状态下拉排除 promoted(立项走独立 promoteToProject 流程,列表状态菜单同排除,
// 状态下拉排除 promoted(立项走独立 promoteToProject 流程,列表状态菜单同排除,
// 见 Ideas.vue onStatusChange 兜底);statusOptions 仍含 promoted 供 label 映射(已转立项的灵感 badge 正常显示)。
const menuStatusOptions = computed(() => props.statusOptions.filter(s => s.value !== 'promoted'))
function closeStatusMenu() {
@@ -502,7 +502,7 @@ async function saveTagsEdit() {
savingTags.value = true
try {
const tags = tagsEditInput.value.split(',').map(t => t.trim()).filter(Boolean)
// WK-16:emit 返回落库结果,失败保持编辑态(成功才关),toast 由父组件处理
// emit 返回落库结果,失败保持编辑态(成功才关),toast 由父组件处理
const ok = await emit('update-tags', JSON.stringify(tags))
if (ok) tagsEditing.value = false
await nextTick()
@@ -520,9 +520,9 @@ function statusLabelKey(status: IdeaStatus): string {
return props.statusOptions.find(s => s.value === status)?.labelKey ?? status
}
// B-260615-25:灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
// 灵感描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
// BUG-2026-07-08:原漏解构 ensureLoaded + onMounted 未调用,冷启动(marked 未被其他组件预热)
// 原漏解构 ensureLoaded + onMounted 未调用,冷启动(marked 未被其他组件预热)
// 时 mdReady 永远 false,renderMd 走 escapeFallback 纯文本降级,Markdown 语法不渲染。
const { rendered: renderedDesc, ensureLoaded: ensureMdLoaded } = useRendered(
() => props.idea.description ?? '',
@@ -548,7 +548,7 @@ interface AdversarialEval {
// 对抗式评估数据持久化在 ai_analysis JSON 字段中,前端解析渲染
//
// WK-15:旧实现直接 JSON.parse 后强转 AdversarialEval,字段缺失/非数值时
// 旧实现直接 JSON.parse 后强转 AdversarialEval,字段缺失/非数值时
// 模板对 undefined*100 调 .toFixed 显 NaN,assessmentClass 对 undefined 调
// .toLowerCase() 崩。这里在解析层补齐数值默认值(防御式归一化),模板直读不二次判空:
// - 数值字段缺失/非数值 → 0
@@ -599,7 +599,7 @@ const localAssessmentLabel = (recommendation: string): string =>
assessmentLabel(t, recommendation)
function sentimentClass(sentiment: number) {
// 统一三档(FR-C2: 原 >=0 与模板 >0 矛盾,net_sentiment=0 时文案负面样式 positive)
// 统一三档原 >=0 与模板 >0 矛盾,net_sentiment=0 时文案负面样式 positive)
if (sentiment > 0) return 'positive'
if (sentiment < 0) return 'negative'
return 'neutral'
+1 -1
View File
@@ -106,7 +106,7 @@ const resizing = ref(false)
// maximized=全宽(flex:1填充) | sidebar=固定宽
const panelStyle = computed(() => {
if (aiStore.state.maximized) return { flex: '1', minWidth: '0' }
// LW-11:内联 width 用 min(panelWidth, 100vw) —— 小屏 media query 的 width:100vw 会被内联样式
// 内联 width 用 min(panelWidth, 100vw) —— 小屏 media query 的 width:100vw 会被内联样式
// 更高优先级覆盖致失效;min() 让窄视口下宽度钳制到 100vw,media query 的 absolute 定位/层级仍生效
return {
width: `min(${panelWidth.value}px, 100vw)`,
+2 -2
View File
@@ -92,7 +92,7 @@ const appSettings = useAppSettingsStore()
// 展开 192px(文字+图标) / 收缩 56px(仅图标,hover tooltip 显示文字)
// 优先级:用户手动设置 > 小窗口自动收缩(≤768px)
const COLLAPSE_THRESHOLD = 768
// LW-10:useSetting 响应式读(loadAll 异步填充缓存后 watch 自动同步刷新)——
// useSetting 响应式读(loadAll 异步填充缓存后 watch 自动同步刷新)——
// 原 onMounted 用 appSettings.get 同步读早于父级 loadAll 完成,重启后偏好丢失
const userCollapsed = appSettings.useSetting('df-sidebar-collapsed', false)
const isNarrowWindow = ref(false) // 小窗口自动收缩标记
@@ -119,7 +119,7 @@ function handleCollapseKeydown(e: KeyboardEvent) {
}
onMounted(async () => {
// LW-10:收缩偏好由 useSetting 响应式恢复(loadAll 填充缓存后 watch 自动同步),移除 onMounted 同步读
// 收缩偏好由 useSetting 响应式恢复(loadAll 填充缓存后 watch 自动同步),移除 onMounted 同步读
// 检查当前窗口宽度
checkWindowWidth()
window.addEventListener('resize', checkWindowWidth)
+2 -2
View File
@@ -135,7 +135,7 @@ async function handleApproval(decision: string) {
}
}
// F-260615-01: 多选审批(UX-260618-17 submitting 防双击保留)
// 多选审批(UX-260618-17 submitting 防双击保留)
async function handleApprovalMulti() {
if (multiDecisions.value.length === 0) return
submitting.value = true
@@ -153,7 +153,7 @@ async function handleApprovalMulti() {
}
}
// F-260615-01: 每次新审批清空多选状态(避免上轮残留)
// 每次新审批清空多选状态(避免上轮残留)
watch(() => pendingApproval.value, (newApproval) => {
if (newApproval) {
multiDecisions.value = []
+2 -2
View File
@@ -86,7 +86,7 @@ import SettingRow from './SettingRow.vue'
const { t } = useI18n()
const appSettings = useAppSettingsStore()
// F-260620: 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
// 接共享 toast 通道(Settings.vue showToast),替代 console.error/warn 用户零反馈
const emit = defineEmits<{ (e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void }>()
// F-260619-03 Phase A: AI 工具文件访问授权目录白名单 UI
@@ -126,7 +126,7 @@ async function pickDir() {
const selected = await open({ directory: true, multiple: false })
if (!selected || Array.isArray(selected)) return
newDir.value = selected as string
// F-260620: 选完即加(消除选目录后需二次点"添加"的摩擦)
// 选完即加(消除选目录后需二次点"添加"的摩擦)
await addDir()
} catch (e) {
console.error('选择目录失败:', e)
+1 -1
View File
@@ -88,7 +88,7 @@ const { t } = useI18n()
const appSettings = useAppSettingsStore()
const emit = defineEmits<{
// P0-2: 对齐 ToastType 加 'success'(本面板暂未用,保类型一致)
// 对齐 ToastType 加 'success'(本面板暂未用,保类型一致)
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void
}>()
@@ -67,9 +67,9 @@ const appSettings = useAppSettingsStore()
const settings = reactive({
llmGlobalConcurrency: appSettings.get<number>('df-llm-global-concurrency', 3),
llmPerConvConcurrency: appSettings.get<number>('df-llm-per-conv-concurrency', 2),
// F-260616-01: Agentic 循环最大轮次,默认 10(与后端 DEFAULT_MAX_AGENT_ITERATIONS 对齐)
// Agentic 循环最大轮次,默认 10(与后端 DEFAULT_MAX_AGENT_ITERATIONS 对齐)
agentMaxIterations: appSettings.get<number>('df-ai-agent-max-iterations', 10),
// F-260616-07: 流式失败重试次数,默认 3(与后端 DEFAULT_MAX_AGENT_RETRIES 对齐)
// 流式失败重试次数,默认 3(与后端 DEFAULT_MAX_AGENT_RETRIES 对齐)
agentMaxRetries: appSettings.get<number>('df-ai-agent-max-retries', 3),
})
@@ -148,7 +148,7 @@ function onRetryChange() {
}
// 同步 Agentic 最大轮次到后端(debounce 300ms)
// F-260616-01: 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
// 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
// 下次发消息才生效(与 llm_concurrency 传 Arc 实时反映的区别)。
// 双 clamp:此处 1-50 + 后端 command 再 clamp 1-50,防越界输入致 loop 失效/失控
let _agentIterTimer: ReturnType<typeof setTimeout> | null = null
@@ -184,7 +184,7 @@ const unlimited = computed({
})
// 同步流式失败重试次数到后端(debounce 300ms)
// F-260616-07: 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
// 后端 loop 入口 load 快照锁定边界——热改后当前 loop 不受影响,
// 下次发消息才生效。双 clamp:此处 0-10 + 后端 command 再 clamp 0-10
let _agentRetryTimer: ReturnType<typeof setTimeout> | null = null
function syncAgentMaxRetries() {
@@ -206,12 +206,12 @@ function syncAgentMaxRetries() {
onMounted(() => {
// 启动同步一次 LLM 并发配置(防刷新页面后后端未恢复用户设定值)
syncConcurrencyConfig()
// F-260616-01: 启动同步一次 Agentic 最大轮次(防刷新页面后后端未恢复用户设定值)
// F-260618-23: App.vue 根 onMounted 已在应用启动时恢复(覆盖用户没先进 Settings 直接用 AI Chat 的场景),
// 启动同步一次 Agentic 最大轮次(防刷新页面后后端未恢复用户设定值)
// App.vue 根 onMounted 已在应用启动时恢复(覆盖用户没先进 Settings 直接用 AI Chat 的场景),
// 此处保留双保险(IPC 幂等值相同无害)
syncAgentMaxIterations()
// F-260616-07: 启动同步一次流式失败重试次数(防刷新页面后后端未恢复用户设定值)
// F-260618-23: 同上,App.vue 根 onMounted 已恢复,此处双保险保留
// 启动同步一次流式失败重试次数(防刷新页面后后端未恢复用户设定值)
// 同上,App.vue 根 onMounted 已恢复,此处双保险保留
syncAgentMaxRetries()
})
+13 -13
View File
@@ -185,7 +185,7 @@ import type { AiProviderConfig, ModelConfig, Modality, Capability, ProbeSource }
import { PROVIDER_PRESETS, CUSTOM_PRESET_ID, findPreset } from './providerPresets'
// ============================================================
// AI 提供商域 — 真实后端 CRUD + F-04c 负载均衡池(enabled / weight)
// AI 提供商域 — 真实后端 CRUD + c 负载均衡池(enabled / weight)
// ============================================================
const { t } = useI18n()
@@ -200,7 +200,7 @@ async function syncAiStoreProviders() {
}
const emit = defineEmits<{
// P0-2: 加 'success'(保存成功绿色 toast)
// 加 'success'(保存成功绿色 toast)
(e: 'toast', msg: string, type: 'error' | 'warning' | 'info' | 'success'): void
(e: 'providers-changed'): void
}>()
@@ -229,7 +229,7 @@ const providerForm = reactive({
// F-01 阶段6:模型拉取 + 列表
fetching: false,
models: [] as ModelConfig[],
// P0-2: 保存失败的持久错误横幅(非自隐 toast)。
// 保存失败的持久错误横幅(非自隐 toast)。
// toast 3s 自隐易被忽略,保存失败时表单内常驻错误条 + 可操作建议,
// 直到下次保存尝试/重开表单才清除。null=无错误。
saveError: null as string | null,
@@ -401,7 +401,7 @@ function openProviderForm(p?: AiProviderConfig) {
providerForm.models = []
}
providerForm.fetching = false
providerForm.saveError = null // P0-2: 重开表单清旧错误横幅
providerForm.saveError = null // 重开表单清旧错误横幅
providerForm.visible = true
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
providerForm.savedSnapshot = formBusinessSnapshot()
@@ -426,7 +426,7 @@ function onPresetChange() {
}
/**
* 测试连接按钮可用条件(P0-1):
* 测试连接按钮可用条件
* - baseUrl 必填(无论新建/编辑,表单值都要有)
* - 新建态(editId 空):api_key 必填(无已保存配置可沿用)
* - 编辑态(editId 有):api_key 可空(留空=沿用已保存密钥)
@@ -438,7 +438,7 @@ const canTestConnection = computed(() => {
})
/**
* 测试连接并拉取模型列表(P0-1)。
* 测试连接并拉取模型列表。
* 用**表单当前值**测试(非 DB 值):新建态直接传表单值;编辑态传表单值 + editId,
* apiKey 留空时后端从已保存配置沿用密钥。一次性测试不写库。
*
@@ -486,7 +486,7 @@ async function fetchModels() {
}
/**
* P0-2: 把后端返回的错误文本分类成「用户可操作的建议」。
* 把后端返回的错误文本分类成「用户可操作的建议」。
*
* 后端 commands/provider.rs 已对密钥相关失败返回友好中文 Err(含原因+建议),
* 这里只做轻量分类挂上"建议下一步",避免裸 Err 文本吓到用户:
@@ -526,14 +526,14 @@ async function saveProvider() {
defaultModel: providerForm.defaultModel,
modelConfigs: providerForm.models,
})
// P0-2: 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
// 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
providerForm.saveError = null
providerForm.visible = false
providerForm.savedSnapshot = '' // L19: 关表单清快照
await loadProviders()
emit('toast', t('settings.toastSaved'), 'success')
} catch (e) {
// P0-2: 失败时①表单保持编辑态(不关)②表单内常驻错误横幅(分类建议,非自隐)
// 失败时①表单保持编辑态(不关)②表单内常驻错误横幅(分类建议,非自隐)
// ③仍发 error toast(用户视线不在表单时仍可见),双通道确保反馈不被忽略。
const raw = errMsg(e)
console.error(t('settings.toastSaveFail', { msg: raw }), e)
@@ -570,10 +570,10 @@ async function setDefaultProvider(id: string) {
}
// ============================================================
// F-260614-04c: 负载均衡池可编辑层(enabled / weight)
// c: 负载均衡池可编辑层(enabled / weight)
// 卡片即时调 IPC(对齐 syncConcurrencyConfig 模式);落库后后端 reload_provider_caps
// 重建 per_provider caps,下条消息即按新配置 acquire。
// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 F-04c 轻量 UPDATE 路径冲突)。
// 不走 saveProvider(全量 INSERT OR REPLACE 会触发 R-PD-1 密钥迁移分支,与 c 轻量 UPDATE 路径冲突)。
// ============================================================
let _poolTimer: ReturnType<typeof setTimeout> | null = null
@@ -693,7 +693,7 @@ onMounted(loadProviders)
}
.detail-value.mask { color: var(--df-text-dim); letter-spacing: 1px; }
/* Provider 卡片 disabled 态(F-260614-04c:enabled=false 视觉弱化) */
/* Provider 卡片 disabled 态(c:enabled=false 视觉弱化) */
.provider-card--disabled { opacity: 0.6; }
.pool-badge {
font-size: 10px;
@@ -718,7 +718,7 @@ onMounted(loadProviders)
.form-field-hint { flex-basis: 100%; margin-left: 82px; font-size: 11px; color: var(--df-text-dim); line-height: 1.5; }
.form-actions { display: flex; justify-content: flex-end; padding-top: 4px; }
/* P0-2: 保存失败持久错误横幅(常驻 form-actions 上方,醒目于自隐 toast) */
/* 保存失败持久错误横幅(常驻 form-actions 上方,醒目于自隐 toast) */
.save-error-banner {
background: var(--df-danger-bg);
border: 0.5px solid var(--df-danger);
+2 -2
View File
@@ -60,7 +60,7 @@ export const PROVIDER_PRESETS: readonly ProviderPreset[] = [
baseUrl: 'https://api.deepseek.com',
defaultModel: 'deepseek-v4-flash',
// 重要:deepseek-chat / deepseek-reasoner 旧名已于 2026-07-24 停用,
// 现分别指向 deepseek-v4-flash 的非思考/思考模式(V4-Flash 2026-07-31 发布)。
// 现分别指向 deepseek-v4-flash 的非思考/思考模式(lash 2026-07-31 发布)。
// 思考模式改 deepseek-v4-flash(传 thinking 参数)或 deepseek-v4-pro。
},
{
@@ -74,7 +74,7 @@ export const PROVIDER_PRESETS: readonly ProviderPreset[] = [
baseUrl: 'https://token.sensenova.cn/v1',
// 官方文档(platform.sensenova.cn/docs)核实:Token Plan API 仅 3 个模型 ——
// sensenova-6.7-flash-lite(多模态对话主力)/ sensenova-u1-fast(图像生成,不能对话)/ deepseek-v4-flash(纯文本对话)。
// 无 U1 / U1-Pro 对话模型(那是官网产品名,API 不提供);旧 SenseNova-Turbo 已退役。
// 无 U1 / ro 对话模型(那是官网产品名,API 不提供);旧 SenseNova-Turbo 已退役。
// 默认填对话主力 sensenova-6.7-flash-lite(文档所有工具接入示例的 Model ID)。
defaultModel: 'sensenova-6.7-flash-lite',
},
+1 -1
View File
@@ -241,7 +241,7 @@ export function setConvState(convId: string | null | undefined, s: ConvState): v
// ── F-09 B 路线 per-conv 流式态(streaming/currentText) ──
//
// 背景(DEC-07a 父④):原 `state.streaming`(bool)/`state.currentText`(string)是 store 全局单例,
// 背景(a 父④):原 `state.streaming`(bool)/`state.currentText`(string)是 store 全局单例,
// F-09 多会话并发下:A 后台生成中切到 B 会话,B 末条 AI 气泡命中 `isLastAi && streaming &&
// currentText` 会渲染 A 残留 currentText(BUG-260624-01 同源根因)。本批改 per-conv Map,各会话
// 独立累积流式文本/流式开关,切会话即切到该会话的 stream state,真并发不串话。
+4 -4
View File
@@ -22,7 +22,7 @@ import { findToolCall } from './aiShared'
import { dispatchApprovalIPC } from './useToolApproval'
/**
* F-260616-06: 审批按钮防抖守卫——同一 id 短期多次点击只发一次 IPC。
* 审批按钮防抖守卫——同一 id 短期多次点击只发一次 IPC。
* IPC 往返 <100ms,300ms 窗口足够覆盖竞态。
*/
const _pendingApprovalIds = new Set<string>()
@@ -30,7 +30,7 @@ const _pendingApprovalIds = new Set<string>()
/**
* 工具审批:保持 pending_approval 让审批卡片可见(按钮 loading 由 ToolCard 本地 ref 持有),
* IPC 失败时改 completed+错误文案。后端回事件后由 useAiEvents 转 completed/rejected。
* B-260616-08:不再乐观置 running——原写法让 .ai-tool-approval 整块消失(切骨架屏),
* 不再乐观置 running——原写法让 .ai-tool-approval 整块消失(切骨架屏),
* 按钮无 loading 中间态、用户无重审入口;loading 现下沉到 ToolCard 局部 ref,语义正确。
*
* path_auth 审批链阶段3b(统一审批模型):**按 tc.kind 分派**两条 IPC——
@@ -46,7 +46,7 @@ const _pendingApprovalIds = new Set<string>()
* @param decision path 类专用('once'|'session'|'always'|'deny');risk 类忽略。调用方须据 tc.kind 传对应入参。
*/
async function approveToolCall(toolCallId: string, approved: boolean, decision?: 'once' | 'session' | 'always' | 'deny') {
// F-260616-06: 防抖——同一 id 仍在处理中则跳过(竞态点击/网络慢重试)
// 防抖——同一 id 仍在处理中则跳过(竞态点击/网络慢重试)
if (_pendingApprovalIds.has(toolCallId)) return
_pendingApprovalIds.add(toolCallId)
try {
@@ -96,7 +96,7 @@ async function approveToolCall(toolCallId: string, approved: boolean, decision?:
// IPC 未送达:用户已点过按钮,清看门狗(避免 130s 后误触发假错误)再回滚到失败态
// TD-260621-03 per-conv:清审批 tc 所属会话的 timer(与上方 reset 同 conv,对称成对)。
clearStreamWatchdog(approvalConvId)
state.queue = [] // B-32:IPC 失败即对话结束,清队列防生成中入队消息静默丢失
state.queue = [] // IPC 失败即对话结束,清队列防生成中入队消息静默丢失
// 仅 IPC 真失败(审批已处理/网络断)走到这里:后端工具失败已改走 emit completed,不进此分支
// 不回滚 pending_approval(会卡死按钮),改设 completed + 错误提示,让用户知晓失败
console.error('[AI] 审批操作未送达后端:', e)
+6 -6
View File
@@ -45,7 +45,7 @@ async function loadConversations() {
await switchConversation(pending)
}
} catch (e) {
// UX-260617-08:网络抖动/IPC 断开时原静默失败,列表可能突空用户不知原因。
// 网络抖动/IPC 断开时原静默失败,列表可能突空用户不知原因。
// 此处保留旧列表(赋值语句抛出,旧 state.conversations 不被覆盖)+ 推错误气泡反馈。
// 仅首次加载(state.conversations 为空)时推气泡,避免拉取/侧栏刷新重试时反复弹错。
console.error('[AI] 加载会话列表失败:', e)
@@ -126,7 +126,7 @@ async function newConversation() {
}
}
// 切换 token:快速连点 A→B 时,后返回的 A 响应按 token 丢弃,防 messages 错配(FR-R1)
// 切换 token:快速连点 A→B 时,后返回的 A 响应按 token 丢弃,防 messages 错配
let _latestSwitchId = 0
// 最近一次切换的切换窗口缓冲(成功提交路径恢复续显用;失败/过期路径丢弃,
// 防陈旧文本在后续切换被误恢复造成内容重复)。由 switchConversation 的 finally 统一填充。
@@ -169,7 +169,7 @@ function parseConvMessages(rawMsgs: any[], fallbackPrefix: string): AiMessage[]
}
}
return rawMsgs
// 过滤 tool 消息 + truncated 软删消息(UX-09:编辑某条 user 后其后消息标 truncated,
// 过滤 tool 消息 + truncated 软删消息编辑某条 user 后其后消息标 truncated,
// 保留 DB 可追溯但从视图移除;前端无 status 字段故按原始 JSON 字段过滤)
.filter((m: any) => m.role !== 'tool' && m.status !== 'truncated')
.map((m: any, i: number) => ({
@@ -177,7 +177,7 @@ function parseConvMessages(rawMsgs: any[], fallbackPrefix: string): AiMessage[]
role: m.role,
content: m.content || '',
model: m.model,
// 用后端持久化的真实时间(BUG-260618-01);老数据无 timestamp 字段回退 Date.now()
// 用后端持久化的真实时间;老数据无 timestamp 字段回退 Date.now()
timestamp: typeof m.timestamp === 'number' ? m.timestamp : Date.now(),
// 消息级 token 回显:assistant 消息若 DB 持久化了 prompt_tokens/completion_tokens
// (V38 迁移后 push_assistant_message 落库的本轮 token),映射回 tokenUsage 供
@@ -350,7 +350,7 @@ export async function switchConversation(id: string, force = false) {
state.messages = parseConvMessages(rawMsgs, 'loaded')
// parse 成功才置 active(保证 active 与 messages 一致)
state.activeConversationId = id
// CSW-P1-4: 切到 round0 进行中的会话时,后端该轮 per_conv 只有 user 消息(assistant 占位
// 切到 round0 进行中的会话时,后端该轮 per_conv 只有 user 消息(assistant 占位
// 轮末才入),末条是 user → 流式 currentText 无 assistant 承载(MessageList isLastAi 不命中,
// flushCurrentText 遇 user break)整轮回复丢失。补前端占位 assistant 气泡承接 currentText,
// 生成完成由 AiCompleted/AiAgentRound 收尾 flushCurrentText 回填 content。
@@ -410,7 +410,7 @@ export async function switchConversation(id: string, force = false) {
// 恢复该对话积压的待审批:重启后后端从审计表重建了挂起审批,
// 此处查回并把对应 toolCard.status 置为待审批,使审批卡片重新可见。
// 按 IPC 返的 kind 渲染:'path' 类显 once/always/deny,'risk' 类显 approve/reject。
// F2-1(AC-EFF-F2-1):纯文本会话跳过挂起查询(省 1 次 IPC + 后端全扫)——仅当目标会话消息
// F2-1纯文本会话跳过挂起查询(省 1 次 IPC + 后端全扫)——仅当目标会话消息
// 含工具卡,或目标会话生成中(可能在途产生待审批)才需要恢复挂起。纯文本/无工具历史直接置空。
const hasToolCalls = state.messages.some(m => (m.toolCalls ?? []).length > 0)
if (!targetGen && !hasToolCalls) {
+8 -8
View File
@@ -19,7 +19,7 @@ import type { AiMessage, AiProviderConfig } from '@/api/types'
const appSettings = useAppSettingsStore()
/** 侧栏宽度可拖拽范围(UX-2025-16)。120=窄屏友好,280=宽屏舒适 */
/** 侧栏宽度可拖拽范围。120=窄屏友好,280=宽屏舒适 */
export const SIDEBAR_WIDTH_MIN = 120
export const SIDEBAR_WIDTH_MAX = 280
export const SIDEBAR_WIDTH_DEFAULT = 200
@@ -44,12 +44,12 @@ function applyUiState(s: any) {
if (s.foldedGroups && typeof s.foldedGroups === 'object') {
state.foldedGroups = { ...state.foldedGroups, ...s.foldedGroups }
}
// 侧栏宽度(UX-2025-16):数值字段,clamp 后写入(与上方 boolean 同级兜底)
// 侧栏宽度数值字段,clamp 后写入(与上方 boolean 同级兜底)
state.sidebarWidth = clampSidebarWidth(s.sidebarWidth)
}
// 启动恢复:从 appSettings(SQLite)读取上次的 UI 布局 + activeProvider(模块级单例 state)
// F-260616-10: activeProvider 恢复对齐 df-ai-active-conv 模式;loadProviders 后还会二次校验
// activeProvider 恢复对齐 df-ai-active-conv 模式;loadProviders 后还会二次校验
// DB is_default(双保险:localStorage 可能与 DB 不同步时以 DB 为准)。
function restoreUiState() {
applyUiState(appSettings.get<any>('df-ai-ui', null))
@@ -79,7 +79,7 @@ export function persistUiState() {
sidebarOpen: state.sidebarOpen,
archivedCollapsed: state.archivedCollapsed,
foldedGroups: state.foldedGroups,
// 侧栏宽度(UX-2025-16):拖拽 mouseup 时由 AiChat 调 persistUiState 落盘
// 侧栏宽度拖拽 mouseup 时由 AiChat 调 persistUiState 落盘
sidebarWidth: state.sidebarWidth,
})
}
@@ -141,8 +141,8 @@ function cyclePanelMode() {
async function loadProviders() {
const list = await aiApi.listProviders()
state.providers = list
// F-260616-10: 重启后 activeProvider=null,从 DB is_default 恢复(后端链路正确,仅前端缺同步)
// CR-34-1: 守卫加有效性检查 — savedProvider 指向已删除 provider 时残留无效 id
// 重启后 activeProvider=null,从 DB is_default 恢复(后端链路正确,仅前端缺同步)
// 守卫加有效性检查 — savedProvider 指向已删除 provider 时残留无效 id
if (!state.activeProvider || !list.some((p: AiProviderConfig) => p.id === state.activeProvider)) {
const def = list.find((p: AiProviderConfig) => p.is_default)
if (def) state.activeProvider = def.id
@@ -162,7 +162,7 @@ async function loadSkills() {
async function setProvider(providerId: string) {
await aiApi.setProvider(providerId)
state.activeProvider = providerId
// F-260616-10: 持久化到 appSettings,重启后恢复(对齐 df-ai-active-conv 模式)
// 持久化到 appSettings,重启后恢复(对齐 df-ai-active-conv 模式)
void appSettings.set('df-ai-active-provider', providerId)
}
@@ -201,7 +201,7 @@ export function useAiPanel() {
loadSkills,
setProvider,
clearChat,
// UX-2025-16:侧栏宽度拖拽
// 侧栏宽度拖拽
setSidebarWidth,
persistUiState,
}
+5 -5
View File
@@ -29,7 +29,7 @@ import { nextMsgId, startApprovalTimer, clearApprovalTimer, clearAllApprovalTime
/// 待发送队列上限(超过抛错提示用户)
const QUEUE_LIMIT = 10
/// 排队超时阈值(ms) — 超过后弹 confirm 让用户选择"强制发送"或继续等(B-260616-02)
/// 排队超时阈值(ms) — 超过后弹 confirm 让用户选择"强制发送"或继续等
const QUEUE_TIMEOUT_MS = 30_000
/**
@@ -134,7 +134,7 @@ async function doSend(text: string, skill?: string, force = false, parts?: Conte
}
/**
* 重新生成最后一条 AI 回复(UX-02)。
* 重新生成最后一条 AI 回复。
*
* 与 doSend 的区别:
* - 不 push 新 user 消息(触发它的 user 消息已在历史末尾)
@@ -190,7 +190,7 @@ async function regenerate() {
}
/**
* 编辑最后一条 user 消息并重新生成(UX-09)。
* 编辑最后一条 user 消息并重新生成。
*
* 与 regenerate 的区别:
* - regenerate:末条须是 assistant,pop 旧 AI 回复后重跑
@@ -497,7 +497,7 @@ async function sendQueuedNow(index: number) {
/** 停止当前生成:本地先复位 streaming,再发停止信号。
* 不依赖后端 AiCompleted 收尾——审批态看门狗已 clear(useAiEvents),若 AiCompleted 竞态丢失则 streaming 永久 true 卡死,故本地兜底。
*
* UX-260617-19: stop IPC 失败时的兜底。本地已先置 streaming=false,但若 stop 信号未送达后端,
* stop IPC 失败时的兜底。本地已先置 streaming=false,但若 stop 信号未送达后端,
* 后端会继续生成并发回 delta/Completed 事件,与用户已感知的「已停止」语义冲突;更坏情况是
* 后端 hang 既不收尾也不发事件(stop_flag 未置位、无 watchdog 兜底则无恢复路径)。
* 故 stop 失败时不 clearStreamWatchdog,反而重启看门狗:后端若继续生成,活跃 delta 会不断重置它
@@ -551,7 +551,7 @@ export function useAiSend() {
}
}
// ARC-06: 模块级监听 ai-drain-queue 事件(由 useAiEvents AiCompleted case 发射)
// 模块级监听 ai-drain-queue 事件(由 useAiEvents AiCompleted case 发射)
// 打破 useAiEvents ↔ useAiSend 循环依赖:events 不再直接 import 调用 drainQueue
let _drainUnlisten: (() => void) | null = null
export async function initDrainQueueListener(): Promise<void> {
+8 -8
View File
@@ -53,7 +53,7 @@ async function detachPanel(convId?: string) {
return
}
// 快照当前生成态,供分离窗口接管(保持正在进行的对话)
// F-09:用 per-conv key 写入(多会话各自快照互不覆盖)
// 用 per-conv key 写入(多会话各自快照互不覆盖)
// 批4 双轨收口:读 getConvState(enum 真相源)派生,替代旧 generatingConvs.has。
// 桥接语义:has(convId)=true 等价于 conv_state∈{generating,stopping,compressed}(非终止三态)。
if (state.streaming && targetConv) {
@@ -67,7 +67,7 @@ async function detachPanel(convId?: string) {
// 主窗口 state 与分离窗口 state 独立(各自 webview 独立 JS context,
// stores/ai.ts 模块级单例仅在同 webview 内共享),清主窗口 state 不影响分离窗口生成态。
// 分离窗口接管生成后,主窗口不应再持该会话生成态残留(防重开面板时 UI 显生成中幽灵/进度条)。
// F-09:仅从 convStates 移除该 conv(其他会话可能仍在生成),不全局清空。
// 仅从 convStates 移除该 conv(其他会话可能仍在生成),不全局清空。
// watchdog 在主窗口 AiChat 卸载(App.vue v-if detached)→ stopListener → clearStreamWatchdog 时已清,
// 此处仅清内存 state 视觉残留;localStorage 快照保留供 resumeInDetached 恢复。
setStreaming(false, { convId: targetConv || null, reason: 'detachPanel-clear-main' })
@@ -77,7 +77,7 @@ async function detachPanel(convId?: string) {
const win = new WebviewWindow(label, {
url,
title: 'DevFlow AI',
width: 600, // U-260618: 加宽(原 520 偏窄),与吸附态 AI_DOCK_WIDTH 一致
width: 600, // 加宽(原 520 偏窄),与吸附态 AI_DOCK_WIDTH 一致
height: 700,
minWidth: 360,
minHeight: 400,
@@ -109,7 +109,7 @@ async function detachPanel(convId?: string) {
}
/** 关闭分离窗口并回到内嵌面板(主窗口侧调用)。
* F-09:多会话并发下可能存在多个 detached 窗口,全部关闭并清各自 per-conv 快照。 */
* 多会话并发下可能存在多个 detached 窗口,全部关闭并清各自 per-conv 快照。 */
async function reattachPanel() {
stopFollowMain()
const { WebviewWindow } = await import('@tauri-apps/api/webviewWindow')
@@ -128,7 +128,7 @@ async function reattachPanel() {
// 时序加固:reattach 关闭分离窗口后,显式停分离窗口残留 listener(分离窗口 close 异步,
// onBeforeUnmount stopListener 可能延迟),防主面板重新挂载 startListener 时与旧 listener 重叠竞态。
stopListener()
// F-09:清所有 per-conv 快照(分离窗口已全关,快照无消费方)
// 清所有 per-conv 快照(分离窗口已全关,快照无消费方)
cleanupAllDetachedSnapshots()
}
@@ -230,12 +230,12 @@ export async function restoreGeneratingState(opts?: { fromMainPanel?: boolean; c
async function resumeInDetached(convId: string | null) {
// R5: 不自动 switchConversation —— 分离窗口可看历史列表,但不主动覆盖主窗口 activeConversationId
// (避免主窗口期间发消息落错会话)。若需切换会话,由用户在分离窗口显式操作触发。
// F-09: 透传 convId 给 restoreGeneratingState,定位正确的 per-conv 快照 key。
// 透传 convId 给 restoreGeneratingState,定位正确的 per-conv 快照 key。
await restoreGeneratingState({ convId: convId || undefined })
}
/** 关闭分离窗口自身(分离窗口侧调用)。
* F-09:从当前 webview 的 label 反推 convId,清自己的 per-conv 快照。 */
* 从当前 webview 的 label 反推 convId,清自己的 per-conv 快照。 */
async function closeDetachedWindow() {
state.docked = false
stopFollowMain()
@@ -275,7 +275,7 @@ async function dockDetached() {
}
}
/** 吸附态 AI 窗口宽度/间隙常量(U-260618) */
/** 吸附态 AI 窗口宽度/间隙常量*/
const AI_DOCK_WIDTH = 600
const AI_DOCK_GAP = 4
// 防 syncToMain 改主窗口 size 触发 onResized 递归回调
+9 -9
View File
@@ -26,9 +26,9 @@ export interface StreamBlock {
}
export interface UseStreamRendererOptions {
/** UX-2025-01:保存当前文字选区(v-html 替换前调) */
/** 保存当前文字选区(v-html 替换前调) */
saveSelection: () => void
/** UX-2025-01:恢复之前保存的选区(DOM 重建后调) */
/** 恢复之前保存的选区(DOM 重建后调) */
restoreSelection: () => void
/** 跟随意图锁存(跟随中则 rAF 回调内滚到真实底部) */
isFollowingBottom: Ref<boolean> | ComputedRef<boolean>
@@ -59,7 +59,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
const _blockCache = new Map<string, string>()
const BLOCK_CACHE_LIMIT = 300
// 流式渲染状态:rAF 节流(多 delta 合并一帧,60fps 封顶防主线程阻塞掉帧)
// 分块渲染:已完成块 DOM 稳定不重建 → 选文字保持(UX-2025-01)
// 分块渲染:已完成块 DOM 稳定不重建 → 选文字保持
const streamingBlocks = ref<StreamBlock[]>([])
let rafId: number | null = null
let lastStreamText = ''
@@ -82,7 +82,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
/// (跨双换行不切),非代码段按双换行切段。
/// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
///
/// CR-260615-04:旧实现用正则 /```[^\n]*\n[\s\S]*?(?:```|$)/ 切块,与 marked 围栏规则
/// 旧实现用正则 /```[^\n]*\n[\s\S]*?(?:```|$)/ 切块,与 marked 围栏规则
/// 不一致——不要求行首、固定 3 反引号,导致行中裸三反引号 / 4+ 反引号嵌套围栏切错,
/// 前块缓存固化错误 html。改用 marked.lexer()(经 getMarked() 取 marked 实例)做围栏
/// 感知切块:lexer 把代码围栏识别为单个 type==='code' token(.raw 含整段围栏),
@@ -113,7 +113,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
/// 末块不缓存(可能不完整,下次 delta 变),每次重 parse 处理未闭合 token
function parseBlockNoCache(block: string): string {
// _marked/_purify 在 loadMarkdown 后才就绪;此处仅在 mdReady 后被流式分支调用。
// UX-260617-25:防御性 null check —— renderStreamingBlocks 顶部已有 mdReady/getMarked/
// 防御性 null check —— renderStreamingBlocks 顶部已有 mdReady/getMarked/
// getPurify 守卫,正常路径 marked/purify 必非 null。但 marked.use(highlightCode) 注入的
// renderer 在动态 import 后仍可能抛异常(hljs 注册竞态/未知语言)→ 此前直接 marked!.parse()
// 会让异常冒泡到 rAF 回调中断流式,表现为生成卡死。这里 null 兜底降级 escapeFallback,
@@ -187,7 +187,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
// 流式过程中每 delta 重 parse 会闪烁/错乱)。降级为纯文本(转义围栏原文),AiCompleted
// 后整段 renderMd 会做完整代码块渲染(此时围栏已闭合)。已完成块不受影响(围栏已闭合)。
const degrade = isTail && isUnclosedCodeFence(b)
// AC-EFF-S1-1:单块回复病态(整回复无 \n\n/围栏边界,末块=全文)超阈值 → 降级纯文本,
// 单块回复病态(整回复无 \n\n/围栏边界,末块=全文)超阈值 → 降级纯文本,
// 防每帧 marked.lexer+parse O(n) 卡顿(流式期间临时纯文本,AiCompleted 后完整 markdown)。
const longSingleBlock = isTail && n === 1 && b.length > SINGLE_BLOCK_TEXT_LIMIT
const html = (degrade || longSingleBlock)
@@ -226,7 +226,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
if (rafId !== null) return
rafId = requestAnimationFrame(() => {
rafId = null
// UX-2025-01: 末块 innerHTML 每帧替换,保存选区
// 末块 innerHTML 每帧替换,保存选区
opts.saveSelection()
streamingBlocks.value = renderStreamingBlocks(lastStreamText)
nextTick(() => {
@@ -245,7 +245,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
streamingBlocks.value = []
lastStreamText = ''
// AC-EFF-S1-1:清增量基线(防新轮/新会话误命中旧帧已完成块)。
// 清增量基线(防新轮/新会话误命中旧帧已完成块)。
_prevStreamText = ''
_prevCompleted = []
_prevTail = ''
@@ -259,7 +259,7 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
/// 清空实例级块缓存(内容寻址 Map + LRU)。会话切换/开关变化时调用,
/// 防旧会话已缓存块被新会话命中(跨会话内容错乱)。仅清缓存,不动 streamingBlocks。
/// AC-EFF-S1-1:同步清增量基线(已完成块引用旧会话内容,防跨会话误复用)。
/// 同步清增量基线(已完成块引用旧会话内容,防跨会话误复用)。
function resetStreamCache(): void {
_blockCache.clear()
_prevStreamText = ''
+9 -9
View File
@@ -1,8 +1,8 @@
/**
* ToolCard 审批状态机 — 从 ToolCard.vue 抽离的审批 UI 逻辑。
*
* 职责:审批按钮 loading 态、审批挂起时长显示(BUG-260624-02)、
* 审批超时兜底 toast(UX-260617-14)、approve/reject/path_auth 三选项处理、
* 职责:审批按钮 loading 态、审批挂起时长显示、
* 审批超时兜底 toast、approve/reject/path_auth 三选项处理、
* status 变化自动复位 + pending 计时。
*
* 不含 store IPC 调用(useAiApproval 负责)和模板渲染——纯状态管理。
@@ -14,14 +14,14 @@ import { aiApi } from '@/api'
import type { AiToolCallInfo } from '@/api/types'
/**
* 审批按钮 loading 超时兜底(B-260616-08)。
* 审批按钮 loading 超时兜底。
* 后端回事件使 tc.status 离开 pending_approval → watch 复位。
* 兜底计时到点复位 + toast 提示(后端 IPC 应在 1~3s 内返回;30s 兜底远足超时后允许用户重试)。
* 与 stream 看门狗(130s)解耦:审批是按钮级交互,不是后台流,不需要对齐后端 120s 空闲超时。
*/
const APPROVE_LOADING_TIMEOUT_MS = 30_000
/** AE-2025-05:High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐) */
/** High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐) */
const HIGH_RISK_TOOLS = new Set<string>([
'delete_task', 'delete_project', 'restore_project', 'purge_project', 'delete_file', 'run_command',
'http_request',
@@ -64,11 +64,11 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
const { t } = useI18n()
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
// ── B-260616-08: 审批 loading 态 ──
// ── 审批 loading 态 ──
const approving = ref(false)
let approvingTimer: ReturnType<typeof setTimeout> | null = null
// ── BUG-260624-02: 审批挂起时长显示 ──
// ── 审批挂起时长显示 ──
const pendingSince = ref<number | null>(null)
const waitSecs = ref(0)
let waitTimer: ReturnType<typeof setInterval> | null = null
@@ -103,7 +103,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
return 'normal'
})
// ── UX-260617-14: 审批超时兜底 toast ──
// ── 审批超时兜底 toast ──
const approveToast = reactive({ visible: false, msg: '' })
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
@@ -114,7 +114,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
_approveToastTimer = setTimeout(() => { approveToast.visible = false }, 3000)
}
/** AE-2025-05:High 风险工具的二次确认文案(按操作类型) */
/** High 风险工具的二次确认文案(按操作类型) */
function highRiskConfirmMsg(name: string): string {
if (name === 'run_command') return t('aiTool.confirmHighExec')
if (name === 'http_request') return t('aiTool.confirmHighHttp')
@@ -134,7 +134,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
// 先锁防重入(含二次确认 await 期间)
approving.value = true
try {
// AE-2025-05:High 风险 → 先二次确认
// High 风险 → 先二次确认
if (HIGH_RISK_TOOLS.has(tc.value.name)) {
const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name))
if (!ok) {
+20 -20
View File
@@ -26,7 +26,7 @@ import type { AiToolCallInfo } from '@/api/types'
/** 工具结果 JSON 已知字段(全可选;list_* 运行时返回数组,靠 Array.isArray 区分) */
export interface ToolResult {
path?: string
// A1-B5: 只读工具(git status/diff/log)失败路径后端返 {success:false,error} 合法 JSON 对象(非 Err),
// 只读工具(git status/diff/log)失败路径后端返 {success:false,error} 合法 JSON 对象(非 Err),
// 前端 isToolFailure 靠此字段判失败(防绿框假成功)
success?: boolean
error?: string
@@ -89,9 +89,9 @@ export interface ToolResult {
body?: string
body_bytes?: number
elapsed_ms?: number
// read_file search 模式(BUG-260625-02):传 search 参数返命中行 matches[{line,content}] 非全文 content
// read_file search 模式传 search 参数返命中行 matches[{line,content}] 非全文 content
search?: string
// grep 跨文件内容搜索结果字段(F-260621):
// grep 跨文件内容搜索结果字段
// - output_mode: content(默认)/ files_with_matches / count
// - matches: content 模式命中行 [{file,line,content,context?}]
// - files: files_with_matches 模式命中文件名数组
@@ -131,9 +131,9 @@ export function shouldKeepOpen(tc: AiToolCallInfo): boolean {
}
/**
* UX-260616-01: 工具执行失败检测(completed 状态下的失败语义)。
* 工具执行失败检测(completed 状态下的失败语义)。
*
* 后端 AR-6:Low 风险工具失败不 emit AiError,改 emit AiToolCallCompleted(result=错误),
* 后端 Low 风险工具失败不 emit AiError,改 emit AiToolCallCompleted(result=错误),
* 故失败工具 status 仍为 'completed',与成功工具视觉同态(原绿色边框+绿色结果框)。
* 此函数区分三类失败:
* - run_command exit_code≠0:结果 JSON 合法,parseResult 命中,看 exit_code 判失败
@@ -164,7 +164,7 @@ export function isToolFailure(tc: AiToolCallInfo): boolean {
if (!r) {
const raw = typeof tc.result === 'string' ? tc.result : ''
if (!raw) return false
// UX-260617-18:收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
// 收窄为行首锚定(原 /执行失败|failed|error[:\s]/i 匹配任意位置的子串,
// 误伤 search_files 命中 error.log / run_command stderr 含 "warning" 等正常产出)。
// 仅当某行以「执行失败」(中文 err_msg 前缀)/「Error:」/「Failed:」开头才判失败。
return /^(执行失败|Error:|Failed:)/m.test(raw)
@@ -295,7 +295,7 @@ export function formatSizeDiff(diff: number | undefined | null): string {
return n >= 0 ? `+${n}` : `${n}`
}
/** SW-260618-19: 合并 stdout/stderr(空串跳过),供 cmdOutput 展开 + combineAndTruncateLines 截断共享 DRY */
/** 合并 stdout/stderr(空串跳过),供 cmdOutput 展开 + combineAndTruncateLines 截断共享 DRY */
export function combineOutputs(stdout: string | undefined, stderr: string | undefined): string {
const parts: string[] = []
if (stdout) parts.push(stdout)
@@ -346,7 +346,7 @@ export function formatFileInfoResult(r: ToolResult): string {
}
let base = t('aiTool.fileInfoSize', { path: r.path || '', size: formatBytes(typeof r.size === 'number' ? r.size : 0) })
const extras: string[] = []
// UX-260618-12: file_info modified 时间(epoch_ms → 客户端 toLocaleString)
// file_info modified 时间(epoch_ms → 客户端 toLocaleString)
if (typeof r.modified === 'number' && r.modified > 0) {
extras.push(t('aiTool.fileInfoModified', { time: new Date(r.modified).toLocaleString() }))
}
@@ -365,17 +365,17 @@ export function formatFileInfoResult(r: ToolResult): string {
/**
* 通用工具结果格式化(非文件类工具)。
* UX-260616-04:统一 switch 覆盖全部工具产出人类可读摘要,不再走裸 JSON 兜底。
* 统一 switch 覆盖全部工具产出人类可读摘要,不再走裸 JSON 兜底。
* read_file/list_directory/write_file 有模板特化分支,不进此函数。
*
* P0-3: switch 17 case 拆为策略表 TOOL_FORMATTERS(name → 纯函数),formatToolResult 查表分发。
* switch 17 case 拆为策略表 TOOL_FORMATTERS(name → 纯函数),formatToolResult 查表分发。
* 新增工具只在此表加一行,零改 formatToolResult 主体。逻辑与原 switch 等价(零行为变更)。
*/
// list_* 系列(list_projects/list_tasks/list_ideas/list_trash)共享格式化:
// 渲染结构化列表替代裸 JSON,头行按 name 选 i18n key,条目取 name/title/id + status/stack 标注。
function formatListResult(r: ToolResult, name: string): string {
// UX-260618-12: list_* 后端返 {items,total,has_more} 对象(非数组)
// list_* 后端返 {items,total,has_more} 对象(非数组)
const items = Array.isArray(r) ? r : (Array.isArray(r.items) ? r.items : [])
const total = typeof r.total === 'number' ? r.total : items.length
const head = name === 'list_projects' ? t('aiTool.projectCount', { n: total })
@@ -398,8 +398,8 @@ function formatListResult(r: ToolResult, name: string): string {
/** advance_task 共享格式化(body + summary 一致): review_rounds>0 显退回累加轮数,否则首推原文案 */
function formatAdvanceTask(r: ToolResult): string {
// UX-260618-09: review_rounds>0 显退回累加轮数,=0 回退原文案(首推无噪音)
// UX-260618-14: 后端返 TaskRecord(含 title),优先显 title,无则回退 id(防裸 UUID)
// review_rounds>0 显退回累加轮数,=0 回退原文案(首推无噪音)
// 后端返 TaskRecord(含 title),优先显 title,无则回退 id(防裸 UUID)
return typeof r.review_rounds === 'number' && r.review_rounds > 0
? t('aiTool.advancedTaskWithRounds', { title: r.title || r.id || '', status: r.status || '', n: r.review_rounds })
: t('aiTool.advancedTask', { title: r.title || r.id || '', status: r.status || '' })
@@ -407,7 +407,7 @@ function formatAdvanceTask(r: ToolResult): string {
/** update_task 共享格式化: updated!==false 显字段更新,否则通用 updated */
function formatUpdateTask(r: ToolResult): string {
// UX-260619-04: title 优先(后端补返 title),无则回退 id(防裸 UUID),对齐 advance_task。
// title 优先(后端补返 title),无则回退 id(防裸 UUID),对齐 advance_task。
return r.updated !== false
? t('aiTool.updatedTaskField', { title: r.title || r.id || '', field: r.field || '' })
: t('aiTool.updated')
@@ -444,12 +444,12 @@ function formatSearchFiles(r: ToolResult): string {
}
/**
* grep 摘要/命中计数标签(F-260621):按 output_mode 取合适文案,三分支:
* grep 摘要/命中计数标签按 output_mode 取合适文案,三分支:
* - content(默认):「命中 N 处」(total = 命中行数,total 缺省回退 matches.length)
* - files_with_matches:「命中 N 文件」(files.length)
* - count:「命中 N 处 / M 文件」(total 行数 + total_files)
*
* 抽公共单一函数 G-1:原 ToolResultBody.vue grepHitLabel 与此处 formatGrep 三分支逐行等价,
* 抽公共单一函数 原 ToolResultBody.vue grepHitLabel 与此处 formatGrep 三分支逐行等价,
* 现 export 此函数,ToolResultBody.vue 复用,消除文案漂移风险。i18n 走 @/i18n/i18n-helpers 类型安全 t。
*/
export function formatGrep(r: ToolResult): string {
@@ -489,7 +489,7 @@ function formatBindDir(r: ToolResult): string {
/** run_workflow 格式化(body 版):无 execution_id 走 formatJson,否则显完整三要素(task_id/target_status/exec) */
function formatRunWorkflow(r: ToolResult): string {
// UX-260618-05: 后端返 {task_id,target_status,execution_id,status,note},显 execution_id 对应执行实例
// 后端返 {task_id,target_status,execution_id,status,note},显 execution_id 对应执行实例
const execId = typeof r.execution_id === 'string' && r.execution_id
? r.execution_id
: (typeof r.id === 'string' ? r.id : '')
@@ -613,10 +613,10 @@ export function formatFetchUrl(r: ToolResult): string {
/**
* completed 工具结果的一句话摘要(折叠态 header 也能看到,避免光秃秃)。
* UX-260616-04:覆盖范围与 formatToolResult 对齐(header+body 一致);
* 覆盖范围与 formatToolResult 对齐(header+body 一致);
* read_file/list_directory/write_file 有模板特化分支,header 摘要仍显示关键计数。
*
* P0-3: switch 拆为策略表 TOOL_SUMMARIES(name → 纯函数),toolResultSummary 查表分发。
* switch 拆为策略表 TOOL_SUMMARIES(name → 纯函数),toolResultSummary 查表分发。
* 与 TOOL_FORMATTERS 不共用:summary 版部分 case 输出更短(create/update_project/restore/purge 仅 summary 有;
* rename/bind 的 false 分支返空串而非 formatJson;run_workflow 用 workflowTriggeredShort;list_* 多 has_more 判断差异)。
* 共享语义的纯函数(formatAdvanceTask/formatUpdateTask/formatDelete/formatDeleteFile/formatPatchFile/
@@ -669,7 +669,7 @@ function summaryBindDir(r: ToolResult): string {
/** run_workflow summary 版:无 execution_id 返空串,有则显 workflowTriggeredShort(body 版显完整三要素) */
function summaryRunWorkflow(r: ToolResult): string {
// UX-260618-05: header 副标题显 execution_id 概要(原 workflowHint「请到工作流页面运行」误导——工作流已触发)
// header 副标题显 execution_id 概要(原 workflowHint「请到工作流页面运行」误导——工作流已触发)
const execId = typeof r.execution_id === 'string' && r.execution_id
? r.execution_id
: (typeof r.id === 'string' ? r.id : '')
+2 -2
View File
@@ -122,7 +122,7 @@ export function toolDisplayName(tc: AiToolCallInfo): string {
*/
export function useToolCardHeader(getTc: () => AiToolCallInfo) {
/**
* 审批参数值展示(AR-3):对项目类工具的 id/project_id 特化——优先回显项目名,查不到则显示
* 审批参数值展示对项目类工具的 id/project_id 特化——优先回显项目名,查不到则显示
* 「项目已不存在」提示 + 原 id。非项目工具或其他 key 走通用 formatArgValue。
*/
function displayArgValue(arg: { key: string; val: unknown }): string {
@@ -136,7 +136,7 @@ export function useToolCardHeader(getTc: () => AiToolCallInfo) {
return tGlobal('aiTool.projectIdNotFound', { id })
}
}
// UX-260618-14:advance_task/run_workflow 的 task_id → 任务标题
// advance_task/run_workflow 的 task_id → 任务标题
const taskArgKey = TASK_ID_TOOL_ARG[tc.name]
if (taskArgKey && arg.key === taskArgKey) {
const id = typeof arg.val === 'string' ? arg.val : ''
+1 -1
View File
@@ -32,7 +32,7 @@ export function useConfirm() {
function confirmDialog(msg: string, dangerLabel = ''): Promise<boolean> {
return new Promise(resolve => {
// CR-260615-31:并发覆盖前一个 confirmDialog(连点删除/confirm 交错)时,
// 并发覆盖前一个 confirmDialog(连点删除/confirm 交错)时,
// 前一个 Promise 永挂(resolve 被覆盖)→ 先 resolve?.(false) 打断前一个视为取消
confirmState.value.resolve?.(false)
confirmState.value.msg = msg
+3 -3
View File
@@ -18,7 +18,7 @@
//! 渲染结果不变。
import { computed, ref, type ComputedRef } from 'vue'
// UX-2025-05: 代码块语法高亮 —— 用 highlight.js/lib/core + 按需 registerLanguage,
// 代码块语法高亮 —— 用 highlight.js/lib/core + 按需 registerLanguage,
// 只打常用 ~16 门语言进包(避免全量 import ~1MB);core+已注册语言 gzip 后远小于全量。
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
@@ -77,7 +77,7 @@ const mdReady = ref(false)
const _mdCache = new Map<string, string>()
/**
* UX-2025-05: marked code renderer 注入 —— 优先按 infostring 指定语言高亮(需已注册),
* marked code renderer 注入 —— 优先按 infostring 指定语言高亮(需已注册),
* 否则 highlightAuto 在已注册语言里猜测。输出 <pre><code class="hljs language-{lang}">,
* 保留 marked 原 code 块外层结构(pre/code),兼容现有 .ai-md pre code 样式与 DOMPurify。
*
@@ -114,7 +114,7 @@ function highlightCode(code: string, infostring: string): string {
async function loadMarkdown() {
const [{ marked }, dp] = await Promise.all([import('marked'), import('dompurify')])
marked.setOptions({ breaks: true, gfm: true })
// UX-2025-05: 注入 code renderer 做语法高亮(marked.use renderer 合并,仅覆盖 code,
// 注入 code renderer 做语法高亮(marked.use renderer 合并,仅覆盖 code,
// 其余 renderer 沿用 marked 默认)。
marked.use({
renderer: {
+1 -1
View File
@@ -14,7 +14,7 @@
import { reactive } from 'vue'
import { useTimerOwnership } from './useTimerOwnership'
// P0-2: 加 'success'(保存成功用绿色 toast,此前成功/中性都用 info 致反馈不明确)
// 加 'success'(保存成功用绿色 toast,此前成功/中性都用 info 致反馈不明确)
export type ToastType = 'info' | 'error' | 'warning' | 'success'
export interface ToastState {
+5 -5
View File
@@ -23,7 +23,7 @@ export const PROJECT_STATUS_LABELS: Record<string, string> = {
}
/** 项目状态 → 卡片 badge 样式 class */
// WK-1:对齐 7 态真实生命周期(planning/in_progress/testing/releasing/completed/paused/cancelled)。
// 对齐 7 态真实生命周期(planning/in_progress/testing/releasing/completed/paused/cancelled)。
// 类名对齐 ProjectCard.vue scoped 的 stage-* 色系(design/coding/testing/release),paused/cancelled 走新增
// stage-dim(置灰)。'active' 键删除:后端无此状态,老数据兜底走 stage-design(与 projectStageInfo 兜底一致)。
export const PROJECT_STATUS_BADGE_CLASS: Record<string, string> = {
@@ -46,7 +46,7 @@ export interface ProjectStageInfo {
stage: string
}
// WK-1:对齐 7 态。stage 值决定 Dashboard dot/chip/fill 颜色档(planning/coding/testing/release/paused/cancelled),
// 对齐 7 态。stage 值决定 Dashboard dot/chip/fill 颜色档(planning/coding/testing/release/paused/cancelled),
// 需与 ActiveProjectsPanel.vue 的 dot-*/chip-*/fill-* 类一一对应(paused 为该组件新增 dim 类)。
// 'active' 键已删,老数据兜底走 projectStageInfo 默认 planning。
export const PROJECT_STAGE_INFO: Record<string, ProjectStageInfo> = {
@@ -68,13 +68,13 @@ export function projectBadgeClass(status: string): string {
}
export function projectStageInfo(status: string): ProjectStageInfo {
// F-09:兜底用字面量而非 PROJECT_STAGE_INFO.planning,避免已删键的隐式依赖
// 兜底用字面量而非 PROJECT_STAGE_INFO.planning,避免已删键的隐式依赖
return PROJECT_STAGE_INFO[status] ?? { stepIndex: 0, progress: 0, stage: 'planning' }
}
// ── 任务状态 ──
// D-260616-01:前端对齐后端 7 态纯状态机(crates/df-core/src/types.rs TaskStatus)
// 前端对齐后端 7 态纯状态机(crates/df-core/src/types.rs TaskStatus)
// 删旧 5 态 Git 工作流导向(review_ready/merged/abandoned)。
// 后端 task.rs update_task 写 status 经 TaskStatus::is_valid 拦截,旧 5 态无法落库;
// migrations.rs V1 无 seed,create_task 默认 todo → 新 DB 无旧 5 态数据,安全对齐。
@@ -129,7 +129,7 @@ export function taskStatusClass(status: string): string {
return TASK_STATUS_CLASS[mapLegacyStatus(status)] ?? 'status-todo'
}
// WK-8:任务状态机合法流转目标(源状态 → 可达目标态,对齐 TaskDetail.vue ADVANCE_MAP 的 target 集)。
// 任务状态机合法流转目标(源状态 → 可达目标态,对齐 TaskDetail.vue ADVANCE_MAP 的 target 集)。
// 列表快捷菜单据此过滤,避免直接置终态/非法跳转(done 为唯一终态,无推进目标;cancelled 可恢复);
// 新建/手动改状态仍走后端 TaskStatus::is_valid 兜底校验。
export const TASK_STATUS_TRANSITIONS: Record<string, string[]> = {
+4 -4
View File
@@ -23,9 +23,9 @@ export default {
errorTimeout: 'The response timed out. Please try again.',
errorNetwork: 'Network connection failed. Please check your network.',
// CR-30-2 / F-260616-07(d) / decision a1: stream pre-failure retry bubble copy
// UX-260616-01: clarify this is an AI response (network/rate-limit) retry, not a tool failure (shown in card)
// clarify this is an AI response (network/rate-limit) retry, not a tool failure (shown in card)
aiStreamRetry: '⚠ AI response failed, retrying ({attempt}/{max})…',
// UX-260616-01: Fatal errors (4xx/auth) are not retryable — prompt user to check settings instead
// Fatal errors (4xx/auth) are not retryable — prompt user to check settings instead
notRetryable: 'This error cannot be resolved by retrying. Please check the configuration.',
streamInterruptedAfterTool: '⚠ The tool finished, but the following reply was interrupted (no data stream for a long time). You can click Continue to retry.',
streamInterrupted: '⚠ The response was interrupted (no data stream for a long time). This may be due to a network disconnection or a backend error. Please try again.',
@@ -38,7 +38,7 @@ export default {
approvalNotDelivered: 'Approval operation failed to reach the backend: {error}',
queuedSendFailed: 'Failed to send queued message: {error}',
approvalTimeout: '⚰ Approval timed out, automatically rejected: {toolName}',
// UX-260617-08: conversation load/switch failure feedback (network jitter/IPC down → empty list/blank, was silent)
// conversation load/switch failure feedback (network jitter/IPC down → empty list/blank, was silent)
loadConvFail: 'Failed to load conversations. Please check your network or backend connection.',
switchConvFail: 'Failed to switch conversation. Please retry.',
// M30: rename/archive/pin IPC failure feedback (was no try/catch, exceptions leaked to unhandledrejection)
@@ -52,7 +52,7 @@ export default {
unarchiveAction: 'unarchive',
pinAction: 'pin',
unpinAction: 'unpin',
// AE-2025-07: Agentic loop progress bar copy
// Agentic loop progress bar copy
// Note: max round / completed tool count are not surfaced by backend this round;
// template conditionally omits them. Add *WithMax keys once backend exposes them.
agenticProgress: '🔄 Round {round} · ⏳{pending} pending',
+8 -8
View File
@@ -71,7 +71,7 @@ export default {
selectModel: 'Select model',
noModels: 'No models available. Please fetch in settings.',
// ── Empty states (UX-2025-20: example prompt cards + no-provider guidance) ──
// ── Empty states example prompt cards + no-provider guidance) ──
emptyNoProvider: 'No AI provider configured',
emptyNoProviderHint: 'Please add a provider in settings to start chatting',
emptyTitle: 'Chat with AI assistant',
@@ -84,7 +84,7 @@ export default {
goToSettings: 'Open settings',
goToSettingsHint: 'The detached window cannot open settings directly. Configure AI providers on the main window’s Settings page.',
// ── Provider switch feedback (UX-2025-14) ──
// ── Provider switch feedback ──
providerSwitched: 'Switched to {model}',
providerSwitchHint: 'Current model: {model}',
@@ -107,7 +107,7 @@ export default {
skillNoMatch: 'No matching skill',
skillNotLoaded: 'No local skills loaded',
// ── UX-10: @ entity mention (@ opens popover, select inserts [Type:Name] token) ──
// ── @ entity mention (@ opens popover, select inserts [Type:Name] token) ──
mentionHint: "{'@'} to mention project/task",
mentionGroupProject: 'Projects',
mentionGroupTask: 'Tasks',
@@ -136,13 +136,13 @@ export default {
// ── Confirm dialog ──
confirmDeleteConversation: 'Delete chat "{title}"? This cannot be undone.',
// UX-260617-24: stronger warning when deleting the currently active conversation
// stronger warning when deleting the currently active conversation
confirmDeleteActiveConversation: '"{title}" is the chat you are currently viewing. Deleting it will clear the current view and cannot be undone. Continue?',
clearChat: 'Clear',
confirmClearChat: 'Are you sure you want to clear all messages in this chat? This cannot be undone.',
toastSendFail: 'Send failed: {msg}',
// ── Message copy (F-260615-13) ──
// ── Message copy ──
copyMsg: 'Copy message',
copied: 'Copied',
copyFailed: 'Copy failed',
@@ -151,14 +151,14 @@ export default {
copiedAsImage: 'Copied as image',
copyAsImageFailed: 'Failed to copy as image',
// ── Message actions bar (UX-02) ──
// ── Message actions bar ──
regenerate: 'Regenerate',
regenerateFailed: 'Regenerate failed: {msg}',
regenerateUnavailable: 'No reply to regenerate',
// fetch_url "Install obscura via AI" button triggers sendMessage; failure toast (mirrors editMessageFailed)
sendFailed: 'Send failed: {msg}',
// ── UX-09: edit last user message and regenerate ──
// ── edit last user message and regenerate ──
editMessage: 'Edit',
editMessageFailed: 'Edit failed: {msg}',
editNotLastUser: 'Only the last user message can be edited',
@@ -168,7 +168,7 @@ export default {
retry: 'Retry',
goToSettingsAction: 'Open settings',
// ── F-260616-03: max iterations reached pause action card ──
// ── max iterations reached pause action card ──
maxRoundsReached: 'Max rounds reached. Continue?',
maxRoundsHint: 'Continue runs another full round; stop ends with current results',
continueLoop: 'Continue',
+17 -17
View File
@@ -6,11 +6,11 @@ export default {
approveAll: 'Approve All ({n})',
rejectAll: 'Reject All',
rejectedHint: 'User rejected this action',
// B-260616-12: tool execution slow hint (frontend fallback, no backend cancel IPC)
// tool execution slow hint (frontend fallback, no backend cancel IPC)
executionSlow: 'Tool "{name}" is taking longer than expected, please wait…',
// BUG-260624-02: approval pending duration hint (>2min orange, >5min red)
// approval pending duration hint (>2min orange, >5min red)
waitTime: '⏱ Waiting {time}',
// UX-260616-01: tool execution failure marker (AR-6 keeps status=completed, red to distinguish from success)
// tool execution failure marker (AR-6 keeps status=completed, red to distinguish from success)
executionFailed: 'Failed',
executionFailedHint: 'Tool execution failed, see details below',
// Singular expand/collapse (ToolCard single-card content, command output toggle; expandAll/collapseAll are for ToolCardList global multi-group)
@@ -31,7 +31,7 @@ export default {
dirFallback: 'List Directory',
writePrefix: 'Write',
writeFallback: 'Write File',
// grep cross-file content search header (F-260621)
// grep cross-file content search header
grepPrefix: 'Search',
grepFallback: 'Content Search',
// CRUD tool display names (delete/restore/purge/update_project resolve project name by id; fallback when no id or unresolved)
@@ -63,26 +63,26 @@ export default {
purged: 'Purged',
purgeIneffective: 'Purge ineffective',
workflowHint: 'Run it on the workflow page',
// UX-260618-05: run_workflow tool_result shows execution_id (workflowHint misleading — workflow already triggered; old key kept for other refs)
// run_workflow tool_result shows execution_id (workflowHint misleading — workflow already triggered; old key kept for other refs)
workflowTriggered: 'Task {id} triggered workflow (advancing to {status}, execution {exec})',
workflowTriggeredShort: 'Execution {exec}',
// UX-260616-04: formatToolResult covers all tools with human-readable summaries (missing keys)
// formatToolResult covers all tools with human-readable summaries (missing keys)
updatedTaskField: 'Task「{title}」 updated field {field}',
advancedTask: 'Task「{title}」 advanced to {status}',
// UX-260618-09: advance_task return-round accumulation (shown only when >0); UX-260618-14: {title} = task name (TaskRecord), fallback id
// advance_task return-round accumulation (shown only when >0); {title} = task name (TaskRecord), fallback id
advancedTaskWithRounds: 'Task「{title}」 advanced to {status} (returned {n} times)',
commandOk: 'Command succeeded ({ms} ms)',
commandFailed: 'Command failed (exit code {code})',
commandOutputLines: '{n} lines of output',
patchedFile: 'Patched {path} ({diff} bytes)',
// UX-260618-06: patch_file multi-match hint + size_diff unit
// patch_file multi-match hint + size_diff unit
patchedMatches: 'matched {n} places',
bytesUnit: 'bytes',
patchedNoChange: '{path} no change',
appendedTo: 'Appended {bytes} to {path}',
renamedFile: 'Renamed {from} → {to}',
// UX-260618-12: rename_file bytes/cross-volume + write_file overwrite/encoding hints
// rename_file bytes/cross-volume + write_file overwrite/encoding hints
renamedWithBytes: 'Renamed {from} → {to} ({bytes})',
renamedCrossVolume: 'Moved {from} → {to} (cross-volume, {bytes})',
writeOverwrite: 'overwrote {old} → {neww}',
@@ -113,7 +113,7 @@ export default {
fetchUrlFallback: 'Fetch URL',
installObscura: 'Install obscura via AI',
obscuraInstallPrompt: 'Please install obscura (a Rust headless browser, used by fetch_url render=true for JS rendering). Try `cargo install obscura` first; if cargo is unavailable, download the prebuilt binary for this platform from the GitHub releases (github.com/h4ckf0r0day/obscura) and place it on PATH. After installing, verify with `obscura --version` and tell me I can retry the web fetch.',
// grep cross-file content search results (F-260621): per output_mode phrasing
// grep cross-file content search results per output_mode phrasing
grepHitsN: '{n} hits',
grepFilesN: '{n} files',
grepCountSummary: '{n} hits / {files} files',
@@ -121,10 +121,10 @@ export default {
grepTruncated: 'Results truncated (max reached), narrow pattern or add glob',
boundDir: 'Bound directory "{path}" stack: {stack}',
deleteFilePermanent: 'Permanently deleted {path}',
// UX-260618-07: soft-delete adds backup_path (trash filename so user knows where to recover from)
// soft-delete adds backup_path (trash filename so user knows where to recover from)
deleteFileSoft: 'Soft-deleted {path} (backed up, recover from: {backup_path})',
fileInfoSize: '{path} {size}',
// UX-260618-12: file_info modified time (epoch_ms → client toLocaleString)
// file_info modified time (epoch_ms → client toLocaleString)
fileInfoModified: ' · modified {time}',
fileInfoLines: ' · {n} lines',
fileInfoBinary: ' · binary',
@@ -133,11 +133,11 @@ export default {
projectLabel: '"{name}"',
projectIdNotFound: '(project not found, id={id})',
// UX-260618-14: advance_task/run_workflow task_id → task title (store.tasks lookup; fallback id)
// advance_task/run_workflow task_id → task title (store.tasks lookup; fallback id)
taskLabel: '"{name}"',
taskIdNotFound: '(task not in current list, id={id})',
// AE-2025-05: High-risk approval second confirm (ask once more before approve; misclick cancellable)
// High-risk approval second confirm (ask once more before approve; misclick cancellable)
confirmHighDelete: 'Permanently delete? This action cannot be undone',
confirmHighExec: 'Run this command?',
confirmHighGeneric: 'Proceed with this high-risk action?',
@@ -145,7 +145,7 @@ export default {
// Tool result detail format (plain-text result + approval placeholder appends arg summary)
resultDetail: '{raw} ({detail})',
// UX-260617-17: ToolCardList group header (shown when 2+ same-name tools; was hardcoded zh)
// ToolCardList group header (shown when 2+ same-name tools; was hardcoded zh)
toolGroup: {
readFile: 'Read File',
writeFile: 'Write File',
@@ -165,10 +165,10 @@ export default {
httpRequest: 'HTTP Request',
},
// UX-260617-14: approval loading fallback timeout notice (no backend ack, card still pending_approval → reset + prompt)
// approval loading fallback timeout notice (no backend ack, card still pending_approval → reset + prompt)
approveLoadingTimeout: 'Approval response timed out, please retry or check the backend',
// UX-260617-18: tool failure detection copy (regex narrowed to line-anchored err_msg prefix)
// tool failure detection copy (regex narrowed to line-anchored err_msg prefix)
// (reuse executionFailed/executionFailedHint, no new key)
// http_request High-risk confirm: external network call, data may leak via URL/body
+2 -2
View File
@@ -49,7 +49,7 @@ export default {
decided: {
human: 'Human',
auto: 'Auto',
// SA-1: cover all backend decided_by sources (different decision paths, more precise than generic 'auto')
// cover all backend decided_by sources (different decision paths, more precise than generic 'auto')
auto_trust: 'Auto (trusted)',
auto_dedup: 'Auto (deduped)',
cache_hit: 'Auto (cached)',
@@ -66,7 +66,7 @@ export default {
convTruncated: 'Content truncated (expand to view full conversation)',
convSelectHint: '← Select a conversation to view full interaction log',
convReasoning: 'Reasoning (thinking)',
// SA-2: conversation role labels (previously hardcoded Chinese, now i18n)
// conversation role labels (previously hardcoded Chinese, now i18n)
convRoleUser: '🧑 User',
convRoleAssistant: '🤖 Assistant',
convRoleTool: '🔧 Tool',
+1 -1
View File
@@ -230,7 +230,7 @@ export default {
confirmDiscardUnsaved: 'The form has unsaved changes. Discard them?',
toastSaved: 'Saved',
toastSaveFail: 'Save failed: {msg}',
// P0-2: save-failure classified guidance (persistent banner, not raw Err text)
// save-failure classified guidance (persistent banner, not raw Err text)
saveErrKeyringTitle: 'Failed to write key to the OS keychain',
saveErrKeyringDetail: 'Original config left unchanged. Backend reason: {msg}\nSuggestion: check the OS keychain/credential store permissions (Windows Credential Manager / macOS Keychain / Linux secret-service daemon), then click "Save" again to retry.',
saveErrGenericTitle: 'Failed to save provider config',
+1 -1
View File
@@ -95,7 +95,7 @@ export default {
desc: 'Description',
branch: 'Branch',
priority: 'Priority',
// F-260619-01: related idea (1:1, optional)
// related idea (1:1, optional)
idea: 'Related Idea',
ideaPlaceholder: 'None (optional)',
titlePlaceholder: 'Enter task title...',
+4 -4
View File
@@ -23,9 +23,9 @@ export default {
errorTimeout: '响应超时,请重试',
errorNetwork: '网络连接失败,请检查网络',
// CR-30-2 / F-260616-07(d) / 决策 a1: 流前失败重试中错误气泡内更新文案
// UX-260616-01: 明确这是 AI 响应(网络/限流)重试,非工具执行失败(工具失败在卡片内显示)
// 明确这是 AI 响应(网络/限流)重试,非工具执行失败(工具失败在卡片内显示)
aiStreamRetry: '⚠ AI 响应失败,正在重试({attempt}/{max})…',
// UX-260616-01: Fatal 类错误(4xx/鉴权)不可重试,提示用户去设置而非重试
// Fatal 类错误(4xx/鉴权)不可重试,提示用户去设置而非重试
notRetryable: '此错误无法通过重试解决,请检查配置',
streamInterruptedAfterTool: '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。',
streamInterrupted: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。',
@@ -38,7 +38,7 @@ export default {
approvalNotDelivered: '审批操作未送达后端:{error}',
queuedSendFailed: '队列消息发送失败:{error}',
approvalTimeout: '⚰ 审批超时,已自动拒绝:{toolName}',
// UX-260617-08:对话加载/切换失败反馈(网络抖动/IPC 断开 → 列表空/切换空白,原静默无反馈)
// 对话加载/切换失败反馈(网络抖动/IPC 断开 → 列表空/切换空白,原静默无反馈)
loadConvFail: '对话列表加载失败,请检查网络或后端连接',
switchConvFail: '对话切换失败,请重试',
// M30:重命名/归档/置顶 IPC 失败反馈(原无 try/catch 异常进 unhandledrejection,用户无感)
@@ -52,7 +52,7 @@ export default {
unarchiveAction: '取消归档',
pinAction: '置顶',
unpinAction: '取消置顶',
// AE-2025-07: Agentic 循环进度条文案
// Agentic 循环进度条文案
// 注:max 轮次/已完成工具数本轮后端暂不透传,模板条件渲染省略;后端补全后新增 *WithMax key
agenticProgress: '🔄 循环 {round} · ⏳{pending}待审批',
agenticProgressNoPending: '🔄 循环 {round} · ✅{completed}个工具完成',
+8 -8
View File
@@ -71,7 +71,7 @@ export default {
selectModel: '选择模型',
noModels: '无可用模型,请在设置中拉取',
// ── 空状态(UX-2025-20:示例问题卡片 + 无 provider 引导) ──
// ── 空状态示例问题卡片 + 无 provider 引导) ──
emptyNoProvider: '未配置 AI 提供商',
emptyNoProviderHint: '请在设置中添加 Provider 后开始对话',
emptyTitle: '与 AI 助手对话',
@@ -84,7 +84,7 @@ export default {
goToSettings: '去设置',
goToSettingsHint: '分离窗口无法直接打开设置,请在主窗口的设置页配置 AI 提供商',
// ── Provider 切换反馈(UX-2025-14) ──
// ── Provider 切换反馈──
providerSwitched: '已切换到 {model}',
providerSwitchHint: '当前模型:{model}',
@@ -107,7 +107,7 @@ export default {
skillNoMatch: '无匹配技能',
skillNotLoaded: '未读取到本机技能',
// ── UX-10: @ 实体引用(@ 触发 popover,选中插入 [类型:名] 标记) ──
// ── @ 实体引用(@ 触发 popover,选中插入 [类型:名] 标记) ──
mentionHint: "{'@'} 引用项目/任务",
mentionGroupProject: '项目',
mentionGroupTask: '任务',
@@ -137,13 +137,13 @@ export default {
// ── 确认弹层 ──
confirmDeleteConversation: '确定删除对话「{title}」?该操作不可恢复。',
// UX-260617-24:删除当前活跃对话的醒目提示(活跃对话误删会清空当前视图,影响更大)
// 删除当前活跃对话的醒目提示(活跃对话误删会清空当前视图,影响更大)
confirmDeleteActiveConversation: '「{title}」是当前正在查看的对话,删除后将清空当前视图且不可恢复,确定继续?',
clearChat: '清空',
confirmClearChat: '确定清空当前对话的所有消息?此操作不可恢复。',
toastSendFail: '发送失败:{msg}',
// ── 消息拷贝(F-260615-13) ──
// ── 消息拷贝──
copyMsg: '复制消息',
copied: '已复制',
copyFailed: '复制失败',
@@ -152,14 +152,14 @@ export default {
copiedAsImage: '已复制为图片',
copyAsImageFailed: '复制为图片失败',
// ── 消息操作栏(UX-02) ──
// ── 消息操作栏──
regenerate: '重新生成',
regenerateFailed: '重新生成失败:{msg}',
regenerateUnavailable: '没有可重新生成的回复',
// fetch_url "AI 帮装 obscura" 按钮触发 sendMessage 失败的 toast(对齐 editMessageFailed 模式)
sendFailed: '发送失败:{msg}',
// ── UX-09: 编辑末条 user 消息并重新生成 ──
// ── 编辑末条 user 消息并重新生成 ──
editMessage: '编辑',
editMessageFailed: '编辑失败:{msg}',
editNotLastUser: '只能编辑最后一条用户消息',
@@ -169,7 +169,7 @@ export default {
retry: '重试',
goToSettingsAction: '去设置',
// ── F-260616-03: 达 max_iterations 暂停态操作卡 ──
// ── 达 max_iterations 暂停态操作卡 ──
maxRoundsReached: '已达最大轮次,是否继续?',
maxRoundsHint: '继续将再跑一轮上限;停止则按当前结果收尾',
continueLoop: '继续',
+17 -17
View File
@@ -6,11 +6,11 @@ export default {
approveAll: '全部批准 {n} 项',
rejectAll: '全部拒绝',
rejectedHint: '用户拒绝了此操作',
// B-260616-12:工具执行超时提示(前端降级,无后端取消 IPC)
// 工具执行超时提示(前端降级,无后端取消 IPC)
executionSlow: '工具「{name}」执行较久,请稍候…',
// BUG-260624-02:审批挂起时长提示(>2分钟橙色,>5分钟红色)
// 审批挂起时长提示(>2分钟橙色,>5分钟红色)
waitTime: '⏱ 已等待 {time}',
// UX-260616-01:工具执行失败视觉标识(AR-6 下 status=completed,需红色区分于成功)
// 工具执行失败视觉标识(AR-6 下 status=completed,需红色区分于成功)
executionFailed: '执行失败',
executionFailedHint: '工具执行失败,详情见下方',
collapse: '收起',
@@ -31,7 +31,7 @@ export default {
dirFallback: '列出目录',
writePrefix: '写入',
writeFallback: '写入文件',
// grep 跨文件内容搜索头部(F-260621)
// grep 跨文件内容搜索头部
grepPrefix: '搜索',
grepFallback: '内容搜索',
// CRUD 类工具显示名(delete/restore/purge/update_project 按 id 解析项目名;无 id 或解析失败走 fallback)
@@ -64,26 +64,26 @@ export default {
purged: '已彻底删除',
purgeIneffective: '清除未生效',
workflowHint: '请到工作流页面运行',
// UX-260618-05:run_workflow tool_result 显 execution_id(workflowHint 误导——工作流已触发;旧 key 保留防他处引用)
// run_workflow tool_result 显 execution_id(workflowHint 误导——工作流已触发;旧 key 保留防他处引用)
workflowTriggered: '任务 {id} 已触发工作流(推进至 {status},执行实例 {exec})',
workflowTriggeredShort: '执行实例 {exec}',
// UX-260616-04:formatToolResult 覆盖全部工具产出人类可读摘要(补缺失 key)
// formatToolResult 覆盖全部工具产出人类可读摘要(补缺失 key)
updatedTaskField: '任务「{title}」已更新字段 {field}',
advancedTask: '任务「{title}」已推进至 {status}',
// UX-260618-09:advance_task 退回累加轮数(>0 才显);UX-260618-14:{title} 显任务名(后端返 TaskRecord),回退 id
// advance_task 退回累加轮数(>0 才显);{title} 显任务名(后端返 TaskRecord),回退 id
advancedTaskWithRounds: '任务「{title}」已推进至 {status}(已退回 {n} 次)',
commandOk: '命令成功({ms} 毫秒)',
commandFailed: '命令失败(退出码 {code})',
commandOutputLines: '{n} 行输出',
patchedFile: '已修改 {path}(增减 {diff} 字节)',
// UX-260618-06:patch_file 多匹配提示 + size_diff 单位
// patch_file 多匹配提示 + size_diff 单位
patchedMatches: '匹配 {n} 处',
bytesUnit: '字节',
patchedNoChange: '{path} 无更改',
appendedTo: '已向 {path} 追加 {bytes}',
renamedFile: '已重命名 {from} → {to}',
// UX-260618-12:rename_file 字节/跨卷 + write_file 覆盖/编码提示
// rename_file 字节/跨卷 + write_file 覆盖/编码提示
renamedWithBytes: '已重命名 {from} → {to}({bytes})',
renamedCrossVolume: '已移动 {from} → {to}(跨卷,{bytes})',
writeOverwrite: '覆盖 {old} → {neww}',
@@ -114,7 +114,7 @@ export default {
fetchUrlFallback: '获取网页',
installObscura: 'AI 帮装 obscura',
obscuraInstallPrompt: '请帮我安装 obscura(Rust 无头浏览器,用于 fetch_url 的 render=true JS 渲染)。优先尝试 `cargo install obscura`,若 cargo 不可用则从 GitHub releases(github.com/h4ckf0r0day/obscura)下载对应平台预编译二进制并放入 PATH。安装完成后用 `obscura --version` 验证,然后告诉我可以重试网页抓取了。',
// grep 跨文件内容搜索结果(F-260621):按 output_mode 分文案
// grep 跨文件内容搜索结果按 output_mode 分文案
grepHitsN: '命中 {n} 处',
grepFilesN: '命中 {n} 文件',
grepCountSummary: '命中 {n} 处 / {files} 文件',
@@ -122,10 +122,10 @@ export default {
grepTruncated: '结果已截断(达上限),收紧 pattern 或加 glob',
boundDir: '已绑定目录「{path}」技术栈:{stack}',
deleteFilePermanent: '已硬删除 {path}',
// UX-260618-07:软删补 backup_path(回收站文件名,用户知从哪恢复)
// 软删补 backup_path(回收站文件名,用户知从哪恢复)
deleteFileSoft: '已软删除 {path}(已备份,可从 {backup_path} 恢复)',
fileInfoSize: '{path} {size}',
// UX-260618-12:file_info 修改时间(epoch_ms → 客户端 toLocaleString)
// file_info 修改时间(epoch_ms → 客户端 toLocaleString)
fileInfoModified: ' · 修改于 {time}',
fileInfoLines: ' · {n} 行',
fileInfoBinary: ' · 二进制',
@@ -135,11 +135,11 @@ export default {
// 审批参数特化(id/project_id 回显项目名,查不到标注原 id)
projectLabel: '「{name}」',
projectIdNotFound: '(项目已不存在, id={id})',
// UX-260618-14:advance_task/run_workflow 的 task_id 回显任务标题(查 store.tasks,无则标 id)
// advance_task/run_workflow 的 task_id 回显任务标题(查 store.tasks,无则标 id)
taskLabel: '「{name}」',
taskIdNotFound: '(任务不在当前列表, id={id})',
// AE-2025-05:High 风险审批二次确认文案(批准前再问一次,误点可取消)
// High 风险审批二次确认文案(批准前再问一次,误点可取消)
confirmHighDelete: '确定永久删除?此操作不可恢复',
confirmHighExec: '确定执行此命令?',
confirmHighGeneric: '确定执行此高危操作?',
@@ -149,7 +149,7 @@ export default {
// 工具结果详情格式(非 JSON 纯文本 + 审批占位时补参数摘要)
resultDetail: '{raw}({detail})',
// UX-260617-17:ToolCardList 分组标题(同名工具 >=2 时显示;原硬编码中文,英文化)
// ToolCardList 分组标题(同名工具 >=2 时显示;原硬编码中文,英文化)
toolGroup: {
readFile: '读取文件',
writeFile: '写入文件',
@@ -169,10 +169,10 @@ export default {
httpRequest: 'HTTP 请求',
},
// UX-260617-14:审批 loading 超时兜底提示(后端无回执,卡片仍 pending_approval 时复位 + 提示用户)
// 审批 loading 超时兜底提示(后端无回执,卡片仍 pending_approval 时复位 + 提示用户)
approveLoadingTimeout: '审批响应超时,请重试或检查后端',
// UX-260617-18:工具失败检测文案(收窄正则后,行首锚定 err_msg 前缀)
// 工具失败检测文案(收窄正则后,行首锚定 err_msg 前缀)
// (复用 executionFailed/executionFailedHint,不新增)
// 审批参数键标签
+2 -2
View File
@@ -49,7 +49,7 @@ export default {
decided: {
human: '人工',
auto: '自动',
// SA-1:补齐后端 decided_by 全部来源(审计记录不同决策路径,精确区分而非笼统 'auto')
// 补齐后端 decided_by 全部来源(审计记录不同决策路径,精确区分而非笼统 'auto')
auto_trust: '自动(信任)',
auto_dedup: '自动(去重)',
cache_hit: '自动(缓存)',
@@ -66,7 +66,7 @@ export default {
convTruncated: '内容过长已截断(展开查看完整对话)',
convSelectHint: '← 选择左侧一次对话查看完整交互日志',
convReasoning: '推理过程(thinking)',
// SA-2:对话角色标签(原硬编码 '🧑 用户' 等,现 i18n 化)
// 对话角色标签(原硬编码 '🧑 用户' 等,现 i18n 化)
convRoleUser: '🧑 用户',
convRoleAssistant: '🤖 助手',
convRoleTool: '🔧 工具',
+1 -1
View File
@@ -229,7 +229,7 @@ export default {
confirmDiscardUnsaved: '表单有未保存的改动,确定放弃吗?',
toastSaved: '已保存',
toastSaveFail: '保存失败:{msg}',
// P0-2: 保存失败分类建议(常驻错误横幅,非裸 Err 文本)
// 保存失败分类建议(常驻错误横幅,非裸 Err 文本)
saveErrKeyringTitle: '密钥写入系统钥匙串失败',
saveErrKeyringDetail: '已保留原配置未改动。后端原因:{msg}\n建议:检查操作系统钥匙串/凭据管理器权限(Windows 凭据管理器 / macOS 钥匙串 / Linux secret-service 守护进程),确认后再次点击「保存」重试。',
saveErrGenericTitle: '保存提供商配置失败',
+1 -1
View File
@@ -95,7 +95,7 @@ export default {
desc: '描述',
branch: '分支',
priority: '优先级',
// F-260619-01:关联灵感(1对1 单向,可选)
// 关联灵感(1对1 单向,可选)
idea: '关联灵感',
ideaPlaceholder: '不关联(可选)',
titlePlaceholder: '输入任务标题...',
+1 -1
View File
@@ -106,7 +106,7 @@ const routes = [
const router = createRouter({
history: createWebHashHistory(),
routes,
// LW-12:路由切换回到顶部(消除滚动残留:从长列表页切到短页仍停在原滚动位置)
// 路由切换回到顶部(消除滚动残留:从长列表页切到短页仍停在原滚动位置)
scrollBehavior: () => ({ top: 0 }),
})
+8 -8
View File
@@ -36,9 +36,9 @@ import type { AiChatEvent, AiConversationSummary, AiMessage, AiProviderConfig, A
const MESSAGE_CAP = 200
/**
* 单对话 messages 的 parts 体积软上限(UX-260617-20)。
* 单对话 messages 的 parts 体积软上限。
*
* 背景:MESSAGE_CAP 仅按"条数"裁剪,不感知单条体积。多模态 base64 图片(F-260614-05)
* 背景:MESSAGE_CAP 仅按"条数"裁剪,不感知单条体积。多模态 base64 图片
* 单条可达数 MB,极端 200 条 × 5MB ≈ 1GB 会撑爆渲染内存。
*
* 设计:
@@ -51,7 +51,7 @@ const PARTS_SIZE_BUDGET = 50 * 1024 * 1024 // 50MB(base64 char 计)
import { useAiEvents } from '@/composables/ai/useAiEvents'
import { useAiStream } from '@/composables/ai/useAiStream'
// 批4 双轨收口:getConvState 从 aiShared 取(下沉破环,避免 stores/ai↔useAiEvents 反向环)。
// F-09 per-conv streaming/currentText(DEC-07a 父④):convStreamStates Map + 辅助函数亦从 aiShared 取。
// F-09 per-conv streaming/currentText(a 父④):convStreamStates Map + 辅助函数亦从 aiShared 取。
import { getConvState, getConvStreamState, setConvStreaming, setConvCurrentText, __bindMessages, __bindState } from '@/composables/ai/aiShared'
import { useAiSend, initDrainQueueListener } from '@/composables/ai/useAiSend'
import { useAiApproval } from '@/composables/ai/useAiApproval'
@@ -62,7 +62,7 @@ import { useAiContext } from '@/composables/ai/useAiContext'
/// 模块级单例 state — 全应用共享(composables 通过 `import { state } from '@/stores/ai'` 取用)
//
// F-09 B 路线 per-conv streaming/currentText(DEC-07a 父④):
// F-09 B 路线 per-conv streaming/currentText(a 父④):
// `streaming`/`currentText` 不再是全局单例字段,改为 accessor 委派 aiShared.convStreamStates
// (per-conv Map,定义于 composables/ai/aiShared.ts)按 activeConversationId 索引读写。
//
@@ -137,7 +137,7 @@ const _stateBase: {
// F-260614-05 Phase 2b: parts 挂多模态图片片(供 drainQueue/force 续发时本地 user 消息渲染图)
queue: [],
// Agentic 循环当前轮次(0=非agentic/未开始;>0=收到后端 AiAgentRound 事件的 round)
// AE-2025-07:进度条用; AiCompleted/AiError 复位为 0
// 进度条用; AiCompleted/AiError 复位为 0
agentRound: 0,
completedTools: 0,
// 归档分组折叠态(默认折叠)
@@ -252,7 +252,7 @@ function estimatePartsSize(parts: ContentPart[] | undefined): number {
}
/**
* parts 体积软上限守卫(UX-260617-20):push 式增长使总 parts 体积超 PARTS_SIZE_BUDGET 时,
* parts 体积软上限守卫push 式增长使总 parts 体积超 PARTS_SIZE_BUDGET 时,
* 从最旧消息起逐条置空其 parts(丢历史多模态图,保文本/条数)。
*
* 仅拦截 push(同上 length watch 的判定),避免切换对话整体替换时误裁后端历史图。
@@ -319,12 +319,12 @@ function isGenerating(convId: string | null | undefined): boolean {
* (composable 内部全部是模块级单例实现,展开的只是引用,无重复实例化开销)。
*/
export function useAiStore() {
// ARC-06: 启动 drain-queue 事件监听(由 useAiEvents AiCompleted 经事件总线触发)
// 启动 drain-queue 事件监听(由 useAiEvents AiCompleted 经事件总线触发)
void initDrainQueueListener()
return {
state,
filteredConversations,
// F-09: 生成态 helper(批4 双轨收口:仅读 isGenerating 派生自 getConvState;
// 生成态 helper(批4 双轨收口:仅读 isGenerating 派生自 getConvState;
// addGenerating/removeGenerating 写 helper 已删,写收敛到 useAiEvents setConvState)
isGenerating,
...useAiEvents(),
+1 -1
View File
@@ -150,7 +150,7 @@ export function useKnowledgeStore() {
}
async function updateStatus(id: string, status: string): Promise<boolean> {
// WK-3:返成功布尔(失败时 runWithCatch 已置 state.error),调用方据此中止推进/报错
// 返成功布尔(失败时 runWithCatch 已置 state.error),调用方据此中止推进/报错
const ok = await runWithCatch(state, t('knowledge.err.updateStatusFailed'), async () => {
await knowledgeApi.updateStatus(id, status)
// 从 items 和 candidates 中同步移除
+9 -9
View File
@@ -28,7 +28,7 @@ function createStore() {
// "df-data-changed" { entity, action },前端按 entity 调对应 load 刷新列表,
// 取代工具执行后需手动刷新。
//
// CR-260615-26(R-1): task 分支不再无参 loadTasks(会把 store.tasks 冲成全量,破坏
// task 分支不再无参 loadTasks(会把 store.tasks 冲成全量,破坏
// B-260615-29「store.tasks 反映当前筛选」契约)。改为读 _activeTaskProject:
// - undefined: Tasks 视图未挂载/从未设过筛选 → 不触发 task load(避免破坏筛选,
// 让 B-29 watch/handle 主导;此时改动由用户下次进 Tasks 自然刷新)
@@ -41,7 +41,7 @@ function createStore() {
// 避免刷新把已选 status 冲掉)。
let _activeTaskProject: string | undefined = undefined
let _activeTaskStatus: string | undefined = undefined
// WK-10:Tasks 视图搜索关键字登记(数据变更联动刷新时一并下沉 keyword,避免冲掉搜索结果)
// Tasks 视图搜索关键字登记(数据变更联动刷新时一并下沉 keyword,避免冲掉搜索结果)
let _activeTaskKeyword: string | undefined = undefined
function setActiveTaskProject(projectKey: string | undefined) {
_activeTaskProject = projectKey
@@ -62,7 +62,7 @@ function createStore() {
return query
}
// WK-4:Ideas 视图当前查询登记(数据变更联动刷新按当前筛选重载,不再无参 loadIdeas 全量冲掉筛选)。
// Ideas 视图当前查询登记(数据变更联动刷新按当前筛选重载,不再无参 loadIdeas 全量冲掉筛选)。
// undefined = Ideas 视图未挂载(如 Dashboard)→ 退化为全量刷新,保持面板统计新鲜。
let _activeIdeaQuery: IdeaQuery | undefined = undefined
function setActiveIdeaQuery(q: IdeaQuery | undefined) {
@@ -75,7 +75,7 @@ function createStore() {
if (_dataChangedUnlisten) return _dataChangedUnlisten
_dataChangedUnlisten = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
const { entity } = event.payload
// 按 entity 调对应 load 刷新(CR-260615-19:action 当前未消费,为未来 delete 本地移除等差异化刷新预留,统一 load 全量拉取)
// 按 entity 调对应 load 刷新action 当前未消费,为未来 delete 本地移除等差异化刷新预留,统一 load 全量拉取)
if (entity === 'project') {
void projectsStore.loadProjects()
} else if (entity === 'task') {
@@ -89,7 +89,7 @@ function createStore() {
// Dashboard 数字(与 Tasks 视图分页/筛选解耦,不受 q undefined 影响)
void loadStats()
} else if (entity === 'idea') {
// WK-4:按 Ideas 视图当前 query 重载(未挂载时退化为全量刷新 Dashboard 面板)
// 按 Ideas 视图当前 query 重载(未挂载时退化为全量刷新 Dashboard 面板)
void ideasStore.loadIdeas(_activeIdeaQuery)
}
})
@@ -182,20 +182,20 @@ function createStore() {
startEventListener: workflowStore.startEventListener,
stopEventListener: workflowStore.stopEventListener,
clearLiveEvents: workflowStore.clearLiveEvents,
// B-41: 工作流进度共享推导(按 execId 派生节点状态/进度)
// 工作流进度共享推导(按 execId 派生节点状态/进度)
workflowProgress: workflowStore.workflowProgress,
approveHumanApproval: workflowStore.approveHumanApproval,
cancelHumanApproval: workflowStore.cancelHumanApproval,
// AR-11(方案A):数据变更联动刷新监听(后端工具执行后 emit df-data-changed)
startDataChangedListener,
stopDataChangedListener,
// CR-260615-26(R-1): Tasks 视图登记当前 activeProject 筛选,供 listener 按筛选拉取
// asks 视图登记当前 activeProject 筛选,供 listener 按筛选拉取
setActiveTaskProject,
// F-260621-02(P1 status 下沉):Tasks 视图登记当前 status 筛选,供 listener 复用同一筛选拉取
setActiveTaskStatus,
// WK-10:Tasks 视图登记当前搜索关键字,供 listener 复用同一筛选拉取
// Tasks 视图登记当前搜索关键字,供 listener 复用同一筛选拉取
setActiveTaskKeyword,
// WK-4: Ideas 视图登记当前查询,供 listener 按筛选重载(不冲掉筛选视图)
// Ideas 视图登记当前查询,供 listener 按筛选重载(不冲掉筛选视图)
setActiveIdeaQuery,
pendingApproval: computed(() => state.pendingApproval),
// computed
+5 -5
View File
@@ -9,11 +9,11 @@ export function createIdeasStore() {
/**
* 加载灵感列表。
*
* F-260621-02:status/keyword/order_by 下沉后端 WHERE,取代前端 computed filter。
* status/keyword/order_by 下沉后端 WHERE,取代前端 computed filter。
* - 传 `query` → 多条件查询(状态/关键词/排序/分页)。
* - 不传 → 全量(created_at DESC,等价旧行为)。
*
* WK-7:成功时清 state.error(load 后不残留旧错误),失败由 runWithCatch 置 error。
* 成功时清 state.error(load 后不残留旧错误),失败由 runWithCatch 置 error。
*/
async function loadIdeas(query?: IdeaQuery) {
state.error = null
@@ -40,7 +40,7 @@ export function createIdeasStore() {
}
async function deleteIdea(id: string): Promise<boolean> {
// WK-7:返成功布尔(失败时 runWithCatch 已置 state.error),调用方据此保留选中态
// 返成功布尔(失败时 runWithCatch 已置 state.error),调用方据此保留选中态
const ok = await runWithCatch(state, t('ideas.err.deleteFailed'), async () => {
await ideaApi.delete(id)
state.ideas = state.ideas.filter(i => i.id !== id)
@@ -57,7 +57,7 @@ export function createIdeasStore() {
}
async function promoteIdea(id: string) {
// WK-4:不再内部全量 loadIdeas(会把 store.ideas 冲成全集、冲掉当前筛选视图)。
// 不再内部全量 loadIdeas(会把 store.ideas 冲成全集、冲掉当前筛选视图)。
// 由视图(Ideas.vue promoteToProject)操作后按当前 buildIdeaQuery() 主动重载。
return await ideaApi.promote(id)
}
@@ -65,7 +65,7 @@ export function createIdeasStore() {
async function relateIdeas(subjectId: string, targetIds: string[]) {
await runWithCatch(state, t('ideas.err.relateFailed'), async () => {
await ideaApi.relateIdeas(subjectId, targetIds)
// WK-4:后端原子更新受影响灵感,原全量 loadIdeas 会冲掉当前筛选视图;
// 后端原子更新受影响灵感,原全量 loadIdeas 会冲掉当前筛选视图;
// 改由视图(Ideas.vue onUpdateRelated)按当前 query 重载。
})
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { state, clearError } from './state'
export function createProjectsStore() {
/**
* 加载未删除项目。
* F-260621-02:可选 query 透传(关键词/排序/分页)。不传 = 当前全量行为(向后兼容)。
* 可选 query 透传(关键词/排序/分页)。不传 = 当前全量行为(向后兼容)。
*/
async function loadProjects(query?: ProjectQuery) {
state.loading = true
+3 -3
View File
@@ -6,14 +6,14 @@ import { state } from './state'
/** 任务 CRUD 子 store(共享全局 state) */
export function createTasksStore() {
// WK-10:并发 loadTasks 竞态守卫(快速切筛选/搜索防旧响应晚到覆盖新结果,
// 并发 loadTasks 竞态守卫(快速切筛选/搜索防旧响应晚到覆盖新结果,
// 对齐 knowledge store 的 _itemsSeq 模式)。旧请求的失败也不污染当前视图。
let _tasksSeq = 0
/**
* 加载任务列表。
*
* **双签名向后兼容**(F-260621-02):
* **双签名向后兼容**
* - `loadTasks(projectId?)`:旧签名,仅项目过滤(等价 `loadTasks({ project_id: projectId })`)。
* - `loadTasks(query?)`:新签名,多条件(status/keyword/...)走后端 WHERE。
*
@@ -58,7 +58,7 @@ export function createTasksStore() {
}
async function deleteTask(id: string): Promise<boolean> {
// WK-13:返成功布尔(失败时 runWithCatch 已置 state.error),快捷删除据此 toast 反馈
// 返成功布尔(失败时 runWithCatch 已置 state.error),快捷删除据此 toast 反馈
const ok = await runWithCatch(state, t('tasks.err.deleteFailed'), async () => {
// 后端已级联软删子任务(delete_task 返回 { ok, cascaded });
// 本地同步移除父任务 + 其直接子任务(1 级嵌套,防悬挂 parent_id)
+15 -15
View File
@@ -3,7 +3,7 @@ import { t } from '@/i18n/i18n-helpers'
import type { NodeStatus, WorkflowEventPayload } from '@/api/types'
import { state, _eventUnlisten, setEventUnlisten } from './state'
// B-41: 工作流进度共享推导。liveEvents 已全局收集所有 workflow-event,
// 工作流进度共享推导。liveEvents 已全局收集所有 workflow-event,
// 此处按 execution_id 重放派生节点状态/进度,供 TaskDetail/ProjectDetail 同源使用,
// 替代各自维护一份 onEvent 累加状态(单事实源,并发工作流互不干扰)。
export interface WorkflowProgress {
@@ -74,7 +74,7 @@ function deriveWorkflowProgress(
return { nodeStatuses, runningNode, doneCount, totalNodes, result }
}
// B-03b-R10(⑦ 互斥): 审批响应/取消 IPC 进行中标记(模块级单例,createWorkflowStore 每会话只跑一次)。
// b-R10(⑦ 互斥): 审批响应/取消 IPC 进行中标记(模块级单例,createWorkflowStore 每会话只跑一次)。
// 防双击/连点在同一轮 IPC 未返回前发起第二次(后端无幂等键,第二次会重复消费 Request 触发
// "node already completed" 类报错)。store 层守卫优于按钮 disabled: 共享单例 state,跨调用方统一。
let _approvalInFlight = false
@@ -112,18 +112,18 @@ export function createWorkflowStore() {
if (_eventUnlisten) return _eventUnlisten
const unlisten = await workflowApi.onEvent((payload) => {
try {
// 固时间戳(FR-C1):formattedEvents computed 重算时用事件发生时刻,非当前时刻
// 固时间戳formattedEvents computed 重算时用事件发生时刻,非当前时刻
state.liveEvents.push({ ...payload, _ts: Date.now() })
// 限长(FR-R3):防 liveEvents 无限增长撑爆内存,保留最近 200 条
// 限长防 liveEvents 无限增长撑爆内存,保留最近 200 条
if (state.liveEvents.length > 200) {
state.liveEvents.splice(0, state.liveEvents.length - 200)
}
// 处理人工审批请求
// WorkflowEvent serde tag=type rename_all=snake_case → type='human_approval_request',字段扁平(无 data 包装)(B-03b-R7)
// WorkflowEvent serde tag=type rename_all=snake_case → type='human_approval_request',字段扁平(无 data 包装)(b-R7)
if (payload.event?.type === 'human_approval_request') {
state.pendingApproval = payload.event as unknown as typeof state.pendingApproval
}
// B-03b-R10(⑤ 终态不清): 工作流走到终态(completed/failed)或审批节点被取消时,
// b-R10(⑤ 终态不清): 工作流走到终态(completed/failed)或审批节点被取消时,
// 若 pendingApproval 仍属于本次 execution,说明该审批未经 approve/cancel 正常路径关闭
// (上游节点失败/工作流被整体取消/用户「稍后」关闭弹窗但 state 仍残留),清掉陈旧态
// 避免下次进 ProjectDetail 时 watch immediate 误判仍有待审批。
@@ -150,7 +150,7 @@ export function createWorkflowStore() {
}
/**
* B-41: 按 execution_id 派生工作流进度(读全局 liveEvents,响应式)。
* 按 execution_id 派生工作流进度(读全局 liveEvents,响应式)。
* 供 TaskDetail(自身推进的 execId)/ ProjectDetail(最近一次执行)共用同源状态。
*/
function workflowProgress(execId: string | null): WorkflowProgress {
@@ -165,25 +165,25 @@ export function createWorkflowStore() {
* - 多选(select_type='multiple'):decisions 透传全量。
*
* CR-11⑪:签名保持 approveHumanApproval(decisions: string[], comment?)。
* F-260615-01: decisions/select_type 透传,与后端 IPC 签名对齐。
* decisions/select_type 透传,与后端 IPC 签名对齐。
*/
async function approveHumanApproval(
decisions: string[],
comment?: string,
) {
if (!state.pendingApproval) return
// B-03b-R10(⑦ 互斥): 上一轮 IPC 未返回,忽略重复点击(连点/双击)
// b-R10(⑦ 互斥): 上一轮 IPC 未返回,忽略重复点击(连点/双击)
if (_approvalInFlight) return
_approvalInFlight = true
const selectType = state.pendingApproval.select_type ?? 'single'
// CR-260615-18:删死逻辑三元(原 selectType 多/单分支返回值相同)
// 删死逻辑三元(原 selectType 多/单分支返回值相同)
// 单选取首项(后端 decision 单值);多选 decision 取首项占位(后端按 decisions 走)
const decision = decisions[0] ?? ''
try {
// R-PD-5: 透传 options 闭环后端校验(options 非空时 decision 必须 ∈ options,否则 IPC 报错)。
// 透传 options 闭环后端校验(options 非空时 decision 必须 ∈ options,否则 IPC 报错)。
// options 来自已收到的 HumanApprovalRequest 事件 payload(IPC 无法访问节点 config)。
// B-260615-34:snake_case 由 api 层对齐后端 workflow.rs:211
// snake_case 由 api 层对齐后端 workflow.rs:211
await workflowApi.approveHumanApproval({
execution_id: state.pendingApproval.execution_id,
node_id: state.pendingApproval.node_id,
@@ -200,7 +200,7 @@ export function createWorkflowStore() {
console.error('审批失败:', error)
state.error = error?.toString() ?? t('projects.err.approveFailed')
} finally {
// B-03b-R10(⑦ 互斥): 无论成功/失败,IPC 已返回,释放重入锁
// b-R10(⑦ 互斥): 无论成功/失败,IPC 已返回,释放重入锁
_approvalInFlight = false
}
}
@@ -208,7 +208,7 @@ export function createWorkflowStore() {
/** 取消当前待审批节点(调 cancel_workflow_node IPC 置节点 Cancelled) */
async function cancelHumanApproval() {
if (!state.pendingApproval) return
// B-03b-R10(⑦ 互斥): 上一轮 IPC 未返回,忽略重复点击
// b-R10(⑦ 互斥): 上一轮 IPC 未返回,忽略重复点击
if (_approvalInFlight) return
_approvalInFlight = true
@@ -223,7 +223,7 @@ export function createWorkflowStore() {
console.error('取消审批失败:', error)
state.error = error?.toString() ?? t('projects.err.cancelFailed')
} finally {
// B-03b-R10(⑦ 互斥): 无论成功/失败,IPC 已返回,释放重入锁
// b-R10(⑦ 互斥): 无论成功/失败,IPC 已返回,释放重入锁
_approvalInFlight = false
}
}
+2 -2
View File
@@ -64,7 +64,7 @@ const ASSESSMENT_CLASS_MAP: Record<string, string> = {
* recommendation 字符串 → CSS badge 类。
* 未知值(或非字符串/空)回退到 'conditional'(中性,对齐原两处实现)。
*
* WK-15:旧实现对 undefined/null 调 `.toLowerCase()` 崩。对抗评估 ai_analysis
* 旧实现对 undefined/null 调 `.toLowerCase()` 崩。对抗评估 ai_analysis
* 可能未生成 recommendation 字段(老数据/降级),调用方传值时也可能漏判空。
* 这里在源头做防御:非字符串/空串统一回退 conditional。
*/
@@ -77,7 +77,7 @@ export function assessmentClass(recommendation: string | null | undefined): stri
* recommendation 字符串 → i18n 本地化标签。
* 命中 ideas.assessment.{recommendation} 则返回译文,否则回退到原始 recommendation。
*
* WK-15:非字符串/空 → 回退占位符 '—'(避免模板渲染 undefined),与详情页其他空值一致。
* 非字符串/空 → 回退占位符 '—'(避免模板渲染 undefined),与详情页其他空值一致。
* @param t vue-i18n 的 t 函数(useI18n().t),由调用方注入避免本模块耦合 i18n 实例
*/
export function assessmentLabel(
+1 -1
View File
@@ -5,7 +5,7 @@
* 仅去除粗体、斜体、删除线、标题、行内代码、代码块围栏、列表、引用、
* 链接、图片等 Markdown 语法标记,保留文字内容。
*
* 动机(UX-260618-13):列表摘要用纯文本插值显示 AI 生成内容,
* 动机列表摘要用纯文本插值显示 AI 生成内容,
* 数据源含 Markdown 语法字符会裸字符外露,列表空间小且配合截断,
* 剥离比富文本渲染更合适。
*/
+1 -1
View File
@@ -13,7 +13,7 @@
import { t, currentLocale } from '@/i18n/i18n-helpers'
// CR-260615-08(P1-10):相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
// CR-260615-08相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
// 类型安全访问经 i18n-helpers 收口(规避 vue-i18n TS2589)。
/** 解析任意时间值为毫秒时间戳;无效或空返回 null(仅本模块内部使用) */
+1 -1
View File
@@ -419,7 +419,7 @@ function decidedClass(d: string): string {
return d === 'human' ? 'decided-human' : 'decided-auto'
}
// SA-2:对话角色 i18n 化(原硬编码 '🧑 用户'/'🤖 助手'/'🔧 工具'/'⚙️ 系统' 不随 locale)
// 对话角色 i18n 化(原硬编码 '🧑 用户'/'🤖 助手'/'🔧 工具'/'⚙️ 系统' 不随 locale)
// user/assistant/tool 映射固定 key,其他未知 role 回退到 '⚙️ 系统'(含 emoji,与其他四态视觉一致)
function convRoleLabel(role: string): string {
if (role === 'user') return t('auditLog.convRoleUser')
+12 -12
View File
@@ -228,7 +228,7 @@ function statusLabelKey(status: IdeaStatus): string {
return statusOptions.find(s => s.value === status)?.labelKey ?? status
}
// ── 后端查询构造(F-260621-02:status/keyword/order_by 下沉后端 WHERE) ──
// ── 后端查询构造status/keyword/order_by 下沉后端 WHERE) ──
//
// status 下沉:单一状态(all→不限 / promoted→promoted)走后端精确匹配;
// hot(按 score≥80)与 pending(多状态 draft+pending_review)无法映射单一后端 status,
@@ -249,7 +249,7 @@ function buildIdeaQuery(): IdeaQuery {
return q
}
// WK-4:按当前视图查询加载(登记 setActiveIdeaQuery 供 df-data-changed 联动刷新复用同一筛选,
// 按当前视图查询加载(登记 setActiveIdeaQuery 供 df-data-changed 联动刷新复用同一筛选,
// 避免 promote/relate/监听触发无参 loadIdeas 全量冲掉当前筛选视图)
function loadCurrentView() {
store.setActiveIdeaQuery(buildIdeaQuery())
@@ -331,14 +331,14 @@ watch([filteredIdeas, () => selectedId.value], () => {
if (targetPage !== page.value) page.value = targetPage
})
// WK-12:分页越界自愈(删除/筛选变化后 page 超界 → 钳制到最后一页,防空白页)
// 分页越界自愈(删除/筛选变化后 page 超界 → 钳制到最后一页,防空白页)
watch([filteredIdeas, pageSize], () => {
if (pageSize.value <= 0) return
const maxPage = Math.max(1, Math.ceil(filteredIdeas.value.length / pageSize.value))
if (page.value > maxPage) page.value = maxPage
})
// B-260615-25:灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器)
// 灵感描述 Markdown 渲染已下沉至 IdeaDetail 子组件(共享模块单例渲染器)
// 评分档位 class(高/中/低 三色标),阈值统一走 scoreTier(单一来源,消除 4 处重复)
function scoreClass(score: number | null) {
@@ -369,7 +369,7 @@ async function confirmCapture() {
tags: JSON.stringify(tags),
source: newIdeaSource.value.trim() || undefined,
})
// WK-18:createIdea 返 null=失败(state.error 已置,顶部 error-banner 展示);成功关弹窗 + toast
// createIdea 返 null=失败(state.error 已置,顶部 error-banner 展示);成功关弹窗 + toast
if (created) {
showCaptureModal.value = false
Message.success(t('ideas.createdToast'))
@@ -384,7 +384,7 @@ async function deleteCurrentIdea() {
if (!await confirmDialog(t('ideas.confirmDelete', { title: currentIdea.value.title }))) return
deleting.value = true
try {
// WK-7:删除失败不清理选中态(错误走 error-banner),仅成功时清空详情
// 删除失败不清理选中态(错误走 error-banner),仅成功时清空详情
const ok = await store.deleteIdea(currentIdea.value.id)
if (ok) selectedId.value = null
} finally {
@@ -397,7 +397,7 @@ async function promoteToProject() {
promoting.value = true
try {
const res = await store.promoteIdea(currentIdea.value.id)
// WK-4:promote 后按当前视图重载(store 不再内部全量 loadIdeas 冲掉筛选)
// promote 后按当前视图重载(store 不再内部全量 loadIdeas 冲掉筛选)
await loadCurrentView()
router.push(`/projects/${res.project_id}`)
} catch (e: any) {
@@ -433,11 +433,11 @@ async function evaluateCurrentIdea() {
async function onStatusChange(newStatus: IdeaStatus) {
if (!currentIdea.value) return
// WK-5:promoted 走独立立项流程(promoteToProject),状态菜单/下拉已排除,此处兜底禁止直接置
// promoted 走独立立项流程(promoteToProject),状态菜单/下拉已排除,此处兜底禁止直接置
if (newStatus === 'promoted') return
// ⑥ 状态切换确认:落到 promoted/rejected/archived 等终态(或离开 approved 失去立项入口)
// 需用户确认,避免误点。中性流转(draft↔pending_review↔approved)直接落库。
// (WK-5:上方已拦截 promoted,此处不再重复比对,TS 收窄后 'promoted' 无交集)
// 上方已拦截 promoted,此处不再重复比对,TS 收窄后 'promoted' 无交集)
const from = currentIdea.value.status
const needsConfirm = newStatus === 'rejected' || newStatus === 'archived'
|| (from === 'approved' && newStatus !== 'approved')
@@ -453,7 +453,7 @@ async function onStatusChange(newStatus: IdeaStatus) {
}
// 接收 IdeaDetail 子组件 emit 的 'update-desc'(描述编辑保存)
// WK-16:返 boolean(成功 true/失败 false),失败 toast;子组件据 false 保持编辑态。
// 返 boolean(成功 true/失败 false),失败 toast;子组件据 false 保持编辑态。
async function onUpdateDesc(desc: string): Promise<boolean> {
if (!currentIdea.value) return false
try {
@@ -470,12 +470,12 @@ async function onUpdateDesc(desc: string): Promise<boolean> {
async function onUpdateRelated(ids: string[]) {
if (!currentIdea.value) return
await store.relateIdeas(currentIdea.value.id, ids)
// WK-4:relate 后端原子更新受影响灵感,按当前视图重载(store 不再内部全量 loadIdeas 冲筛选)
// relate 后端原子更新受影响灵感,按当前视图重载(store 不再内部全量 loadIdeas 冲筛选)
await loadCurrentView()
}
// 接收 IdeaDetail 子组件 emit 的 'update-tags'(标签编辑保存)
// WK-16:返 boolean(成功 true/失败 false),失败 toast;子组件据 false 保持编辑态。
// 返 boolean(成功 true/失败 false),失败 toast;子组件据 false 保持编辑态。
async function onUpdateTags(tags: string): Promise<boolean> {
if (!currentIdea.value) return false
try {
+3 -3
View File
@@ -241,7 +241,7 @@ function getCategoryCount(key: string): number {
// ===== 详情选中 + 加载 =====
const selectedId = ref<string | null>(null)
const detail = ref<KnowledgeDetailPayload | null>(null)
// B-260615-25:Markdown 渲染器预热保留在父级(模块单例,与 AiChat/TaskDetail/KnowledgeDetail 共享),
// Markdown 渲染器预热保留在父级(模块单例,与 AiChat/TaskDetail/KnowledgeDetail 共享),
// 详情内容渲染由 KnowledgeDetail 子组件通过 useRendered 消费,此处仅 ensureLoaded 预热不阻塞首屏
const { ensureLoaded } = useRendered(() => '')
@@ -286,8 +286,8 @@ async function onDetailSave(payload: { id: string; data: { title: string; conten
// ===== 审核操作(收件箱) =====
// P0-②:发布/拒绝/归档后自动选中下一条(而非清空详情),减少审核者来回点击
// WK-2:index 须在 store 操作前快照(操作后 candidates 已 filter 移除 currentId,findIndex 恒 -1)
// WK-3:操作失败(updateStatus/archive 返 false)toast 报错 + 中止推进(不 selectNext,不清详情)
// index 须在 store 操作前快照(操作后 candidates 已 filter 移除 currentId,findIndex 恒 -1)
// 操作失败(updateStatus/archive 返 false)toast 报错 + 中止推进(不 selectNext,不清详情)
async function publishCurrent() {
if (!detail.value) return
const currentId = detail.value.knowledge.id
+6 -7
View File
@@ -556,14 +556,13 @@ const currentProject = computed(() =>
const loading = computed(() => store.projects.length === 0 && !store.error)
// 状态文案/阶段进度统一走 ../constants/project
// B-260615-25:项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
// 项目描述 Markdown 渲染(复用 AiChat/TaskDetail 同款渲染器,模块级单例),
// useRendered 封装 computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
const { rendered: renderedDesc, ensureLoaded } = useRendered(
() => currentProject.value?.description ?? '',
)
const stageKey = computed(() => currentProject.value?.status ?? 'planning')
const statusLabel = computed(() => projectStatusLabel(stageKey.value))
// const currentStageIndex = computed(() => projectStageInfo(stageKey.value).stepIndex)
// ── 来源灵感(晋升携带评估结论回溯)──
// ProjectRecord.idea_id 存灵感 id(promote_idea 写入)。
@@ -850,7 +849,7 @@ async function onScanSubmodules() {
}
// ── 工作流 ──
// demoDag + runDemoWorkflow 已下线(R-PD-2):"script" 节点不再注册到 NodeRegistry,
// demoDag + runDemoWorkflow 已下线"script" 节点不再注册到 NodeRegistry,
// 前端构造含 script 节点的 DagDef 会在后端 build_dag 失败报错。
// 工作流执行日志面板保留,实时事件展示能力不依赖 demoDag。
// 待真实工作流需求落地(独立 BuildNode + 审批链)时重建演示入口。
@@ -868,7 +867,7 @@ const formattedEvents = computed<LogEntry[]>(() => {
const type = ev.type ?? 'unknown'
const level = type.includes('error') || type.includes('fail') ? 'error'
: type.includes('warn') ? 'warn' : 'info'
// 用事件入数组时固化的时间戳(FR-C1),非 computed 重算的当前时刻
// 用事件入数组时固化的时间戳,非 computed 重算的当前时刻
const time = new Date(evt._ts).toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
const message = ev.label
? `[${ev.label}] ${type}: ${ev.output ?? ev.code ?? ev.message ?? JSON.stringify(ev)}`
@@ -1013,8 +1012,8 @@ async function handleImportDir() {
// ── 审批处理 ──
// 审批对话框 UI + 单/多选决策逻辑抽离至 components/project/ApprovalDialog.vue
// handleApproval/handleApprovalMulti/isMultipleSelect/multiDecisions 已迁移;
// submitting(UX-260618-17)在子组件自治,父级 submitting 仅用于 submitNewTask
// P0-C:ApprovalDialog + watch pendingApproval + handleCancelApproval 已提至 App.vue(全局),
// submitting在子组件自治,父级 submitting 仅用于 submitNewTask
// ApprovalDialog + watch pendingApproval + handleCancelApproval 已提至 App.vue(全局),
// TaskDetail 等无 ApprovalDialog 的页面自动受益(原局部渲染致 HumanNode 超时失败)
// ── 删除项目(软删 → 回收站,可恢复)──
@@ -1115,7 +1114,7 @@ onUnmounted(() => {
gap: var(--df-gap-grid);
}
/* B-260615-31:字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状)
/* 字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状)
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
此处仅保留本组件特有覆盖。 */
.info-item a.idea-link {
+2 -2
View File
@@ -219,7 +219,7 @@ const pagedProjects = computed(() => {
return store.projects.slice(start, start + pageSize.value)
})
// WK-12:分页越界自愈(删除/回收后 store.projects 数量变化,page 超界 → 钳制到最后一页,防空白页)
// 分页越界自愈(删除/回收后 store.projects 数量变化,page 超界 → 钳制到最后一页,防空白页)
watch([() => store.projects, pageSize], () => {
if (pageSize.value <= 0) return
const maxPage = Math.max(1, Math.ceil(store.projects.length / pageSize.value))
@@ -598,6 +598,6 @@ async function runImport() {
}
.toast-info { background: var(--df-accent); color: #fff; }
.toast-error { background: var(--df-danger); color: #fff; }
/* P0-2: ToastType 新增 'success' 兜底样式(本视图当前未用,补全避免裸类) */
/* ToastType 新增 'success' 兜底样式(本视图当前未用,补全避免裸类) */
.toast-success { background: var(--df-success); color: #fff; }
</style>
+21 -21
View File
@@ -312,14 +312,14 @@ import { useTaskBatchActions } from '@/composables/task/useTaskBatchActions'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
// B-41: 共享 workflow store(liveEvents 全局收集 + workflowProgress 派生进度)
// 共享 workflow store(liveEvents 全局收集 + workflowProgress 派生进度)
const store = useProjectStore()
const loading = ref(true)
const errorMsg = ref('')
const task = ref<TaskRecord | null>(null)
const projects = ref<ProjectRecord[]>([])
// F-260619-01:灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式)
// 灵感列表用于解析 task.idea_id → 灵感 title(独立入口,同 projects 模式)
const ideas = ref<IdeaRecord[]>([])
const advancing = ref(false)
@@ -471,7 +471,7 @@ async function setChildPriority(child: TaskRecord, priority: number) {
}
}
// F-260805:子任务新建小弹窗
// 子任务新建小弹窗
const showSubtaskModal = ref(false)
const subtaskTitle = ref('')
const subtaskDesc = ref('')
@@ -519,7 +519,7 @@ async function confirmSubtask() {
const wfAdvancing = ref(false)
const wfExecId = ref<string | null>(null)
const wfResult = ref<'completed' | 'failed' | null>(null)
// SW-260618-21: 终态提示 timer 引用,卸载时清理防写已销毁 ref(对齐 Projects.vue _toastTimer 模式)
// 终态提示 timer 引用,卸载时清理防写已销毁 ref(对齐 Projects.vue _toastTimer 模式)
let _wfResultTimer: ReturnType<typeof setTimeout> | null = null
/** 重置工作流推进相关 UI 态(切任务/refresh/手动推进成功后调用。
@@ -531,7 +531,7 @@ function resetWorkflowUi() {
if (_wfResultTimer) { clearTimeout(_wfResultTimer); _wfResultTimer = null }
}
// B-41: 共享工作流进度推导。workflow store 已全局收集 liveEvents,此处按 execId
// 共享工作流进度推导。workflow store 已全局收集 liveEvents,此处按 execId
// 派生节点状态/进度,与 ProjectDetail 同源(store.workflowProgress),替代原本地 onEvent 逐事件累加。
const wfProgress = computed(() => store.workflowProgress(wfExecId.value))
const wfNodeStatuses = computed(() => wfProgress.value.nodeStatuses)
@@ -567,7 +567,7 @@ async function refreshWorkflowDag(execId: string) {
const taskId = computed(() => route.params.id as string)
// B-24:任务描述 Markdown 渲染(复用 AiChat 同款渲染器,模块级单例),useRendered 封装
// 任务描述 Markdown 渲染(复用 AiChat 同款渲染器,模块级单例),useRendered 封装
// computed(读 mdReady 触发响应式 + renderMd)+ ensureLoaded(幂等预热)
const { rendered: renderedDesc, ensureLoaded } = useRendered(
() => task.value?.description ?? '',
@@ -604,7 +604,7 @@ async function measureDescHeight() {
descOverflow.value = el.scrollHeight > DESC_COLLAPSE_PX
}
// P2-10: 描述编辑(对齐 IdeaDetail 模式;taskApi.update(id, 'description', value))
// 描述编辑(对齐 IdeaDetail 模式;taskApi.update(id, 'description', value))
const editingDesc = ref(false)
const editDesc = ref('')
const savingDesc = ref(false)
@@ -636,7 +636,7 @@ const projectName = computed(() => {
return p?.name ?? task.value?.project_id ?? '—'
})
// F-260619-01:解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击)
// 解析 task.idea_id → 灵感 title(找不到时回退 id,保证非空可点击)
const ideaTitle = computed(() => {
const ideaId = task.value?.idea_id
if (!ideaId) return '—'
@@ -670,7 +670,7 @@ const advanceActions = computed<AdvanceAction[]>(() => {
// ADVANCE_MAP 各行的首个 primary 项:todo→in_progress, in_review→testing, testing→done, blocked→in_progress
// in_progress 的 primary 是 in_review(无模板,不显示工作流推进按钮)。
const WF_SUPPORTED_TARGETS = new Set(['in_progress', 'testing', 'done'])
// CR-08-O1: blocked 是阻塞态(任务被阻塞),其 primary=in_progress 虽 ∈ WF_SUPPORTED_TARGETS,
// blocked 是阻塞态(任务被阻塞),其 primary=in_progress 虽 ∈ WF_SUPPORTED_TARGETS,
// 但 template_for('in_progress') 是「todo→in_progress 从头执行」拓扑(AiNode 等重新跑一遍),
// blocked 恢复语义不该重跑 AiNode(任务已有上下文/进度,只需恢复非从头执行)。
// 最小修法:blocked 不显示工作流推进按钮,用户手动 advance(blocked→in_progress 走后端状态机)或先解除阻塞。
@@ -693,7 +693,7 @@ async function handleAdvance(target: string) {
// advance_task 返回更新后的 TaskRecord(含新 status / review_rounds)
const updated = await taskApi.advance(task.value.id, target)
task.value = updated
// P2-8: 手动推进成功后清工作流 UI 态(防上一轮工作流残留进度/终态提示污染新任务态)
// 手动推进成功后清工作流 UI 态(防上一轮工作流残留进度/终态提示污染新任务态)
resetWorkflowUi()
errorMsg.value = ''
} catch (e: any) {
@@ -704,7 +704,7 @@ async function handleAdvance(target: string) {
}
// F-260616-06 ①-1: 工作流推进 — 传空 dag + target_status,后端自动选推进链模板。
// B-41: 监听 workflow-event 显示轻量进度;完成/失败后任务态由后端回调推进(②-3/②-4),
// 监听 workflow-event 显示轻量进度;完成/失败后任务态由后端回调推进(②-3/②-4),
// df-data-changed 自动刷新 task(AR-11 已通),此处不直接改 task.value。
// name 用推进链标识(模板内未硬编码 task_id,运行时由后端 config 注入,前端只传 topology hint)。
async function handleWorkflowAdvance(target: string) {
@@ -729,7 +729,7 @@ async function handleWorkflowAdvance(target: string) {
}
}
// B-41: 进度状态已由共享 store 按 execId 派生(store.workflowProgress),不再本地逐事件累加。
// 进度状态已由共享 store 按 execId 派生(store.workflowProgress),不再本地逐事件累加。
// 原有 handleWorkflowEvent 已移除 —— 事件仍由 workflow store 全局收集,TaskDetail 只读派生结果。
async function load() {
@@ -740,13 +740,13 @@ async function load() {
taskApi.get(taskId.value),
// 项目列表用于解析 project_id → 项目名(独立入口,不依赖全局 store)
projectApi.list(),
// F-260619-01:灵感列表用于解析 idea_id → 灵感 title
// 灵感列表用于解析 idea_id → 灵感 title
ideaApi.list(),
])
task.value = t
projects.value = ps
ideas.value = ideasList
// F-260805:父子数据(子任务取父标题 / 顶层任务取子任务列表),与主数据并行
// 父子数据(子任务取父标题 / 顶层任务取子任务列表),与主数据并行
await Promise.all([loadParentTitle(), loadChildren()])
} catch (e: any) {
task.value = null
@@ -769,7 +769,7 @@ watch(taskId, () => { resetWorkflowUi(); load() })
watch(() => task.value?.id, () => { descExpanded.value = false })
watch(renderedDesc, () => { measureDescHeight() })
// B-260616-18: 数据变更联动刷新 unlistener(onMounted 注册,onBeforeUnmount 释放)
// 数据变更联动刷新 unlistener(onMounted 注册,onBeforeUnmount 释放)
// 对齐 AiChat _unlistenToolSlow 生命周期模式。本视图绕 store 直调 taskApi.get/projectApi.list,
// 不享受 store 全局 df-data-changed 监听(该监听只刷 store.tasks 列表,不含本视图的当前 task 单体),
// 故此本地监听 entity=task/project 时重载当前 task。
@@ -778,9 +778,9 @@ let _unlistenDataChanged: (() => void) | null = null
onMounted(async () => {
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat 共享),不阻塞 load
load()
// F-260805:点击子任务快捷菜单外关闭菜单
// 点击子任务快捷菜单外关闭菜单
document.addEventListener('click', closeChildMenu)
// B-260616-18: 后端 AI 工具(create/update/delete 等)emit df-data-changed → 本视图重载当前 task
// 后端 AI 工具(create/update/delete 等)emit df-data-changed → 本视图重载当前 task
// entity=task:任务本体字段(标题/状态/描述/分支…)被改时刷新;entity=project:项目名变更影响 projectName 解析时刷新
try {
_unlistenDataChanged = await listen<DfDataChangedPayload>('df-data-changed', (event) => {
@@ -792,7 +792,7 @@ onMounted(async () => {
} catch (e) {
console.error('[TaskDetail] 启动 df-data-changed 监听失败:', e)
}
// B-41: 工作流进度由 workflow store 全局 liveEvents 推导(store.workflowProgress),
// 工作流进度由 workflow store 全局 liveEvents 推导(store.workflowProgress),
// 此处只需确保 store 事件监听已启动(幂等,共享单例;按 execution_id 过滤由派生层完成)。
try {
await store.startEventListener()
@@ -803,9 +803,9 @@ onMounted(async () => {
onBeforeUnmount(() => {
if (_unlistenDataChanged) { _unlistenDataChanged(); _unlistenDataChanged = null }
// F-260805:移除子任务快捷菜单关闭监听
// 移除子任务快捷菜单关闭监听
document.removeEventListener('click', closeChildMenu)
// SW-260618-21: 清终态提示 timer 防卸载后写已销毁 ref
// 清终态提示 timer 防卸载后写已销毁 ref
if (_wfResultTimer) { clearTimeout(_wfResultTimer); _wfResultTimer = null }
})
</script>
@@ -903,7 +903,7 @@ onBeforeUnmount(() => {
flex-direction: column;
gap: var(--df-gap-grid);
}
/* B-260615-31:字段同行布局(label 固定宽 + value 占余),描述字段 info-block 保持块状)
/* 字段同行布局(label 固定宽 + value 占余),描述字段 info-block 保持块状)
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
此处仅保留本组件特有覆盖。 */
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li)。
+21 -21
View File
@@ -417,7 +417,7 @@ const router = useRouter()
const store = useProjectStore()
const { t } = useI18n()
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
// WK-13:快捷操作失败 toast 反馈(对齐 Projects.vue useToast 模式)
// 快捷操作失败 toast 反馈(对齐 Projects.vue useToast 模式)
const { toast, showToast } = useToast()
// 列表筛选/分页状态持久化到 localStorage(key 前缀 'tasks.'),
@@ -447,14 +447,14 @@ function toggleStatus(key: string) {
const sortBy = usePersistedRef('tasks.sortBy', 'updated_at')
const searchKeyword = usePersistedRef('tasks.searchKeyword', '')
const loading = ref(false)
// WK-11:搜索态(keyword 过滤可能截断父子关系,子进度徽章/迷你条语义失真 → 隐藏)
// 搜索态(keyword 过滤可能截断父子关系,子进度徽章/迷你条语义失真 → 隐藏)
const isSearching = computed(() => searchKeyword.value.trim().length > 0)
// F-260805 D5:列表页一次性加载 + 前端组装树,移除真分页(page/pageSize/totalTasks 已删)。
// 分组折叠状态(localStorage 记忆)
const collapsedGroups = reactive(new Set<string>())
// F-260805:父任务展开/折叠状态(localStorage 记忆,沿用 collapsedGroups 模式)
// 父任务展开/折叠状态(localStorage 记忆,沿用 collapsedGroups 模式)
const expandedParents = reactive(new Set<string>())
function toggleParent(id: string) {
if (expandedParents.has(id)) {
@@ -491,7 +491,7 @@ const STATUS_FILTER_META: Record<string, { icon: string; labelKey: string }> = {
const quickStatuses = computed(() =>
TASK_STATUS_ORDER.map(key => ({ key, icon: STATUS_FILTER_META[key].icon, label: t(STATUS_FILTER_META[key].labelKey) })),
)
/** WK-8:快捷状态目标 = 状态机合法流转目标(TASK_STATUS_TRANSITIONS 过滤,对齐 TaskDetail ADVANCE_MAP) */
/** 快捷状态目标 = 状态机合法流转目标(TASK_STATUS_TRANSITIONS 过滤,对齐 TaskDetail ADVANCE_MAP) */
function quickStatusesFor(task: TaskRecord): { key: string; icon: string; label: string }[] {
const valid = new Set(TASK_STATUS_TRANSITIONS[task.status] ?? [])
return quickStatuses.value.filter(s => valid.has(s.key))
@@ -508,14 +508,14 @@ async function quickAdvance(id: string, target: string) {
await taskApi.advance(id, target)
await store.loadTasks(buildTaskQuery())
} catch (e: any) {
// WK-13:快捷操作失败 toast 反馈(原 console.error 静默,用户无感知)
// 快捷操作失败 toast 反馈(原 console.error 静默,用户无感知)
showToast(e?.toString() ?? t('common.unknownError'), 'error')
}
}
async function quickPriority(id: string, priority: number) {
quickMenuId.value = null
try {
// WK-13:改用 taskApi 直调(不走 store.updateTask,避免失败置 store.error 触发整表错误态)
// 改用 taskApi 直调(不走 store.updateTask,避免失败置 store.error 触发整表错误态)
await taskApi.update(id, 'priority', String(priority))
await store.loadTasks(buildTaskQuery())
} catch (e: any) {
@@ -524,13 +524,13 @@ async function quickPriority(id: string, priority: number) {
}
async function quickDelete(task: TaskRecord, childCount = 0) {
quickMenuId.value = null
// F-260805:父任务带子任务时确认文案含子任务数(后端级联软删)
// 父任务带子任务时确认文案含子任务数(后端级联软删)
const msg = childCount > 0
? t('tasks.confirmDeleteWithChildren', { title: task.title, n: childCount })
: t('tasks.confirmDelete', { title: task.title })
if (!await confirmDialog(msg)) return
try {
// WK-13:deleteTask 返成功布尔,失败 toast(store.error 同时走 error-banner)
// deleteTask 返成功布尔,失败 toast(store.error 同时走 error-banner)
const ok = await store.deleteTask(task.id)
if (!ok) showToast(t('tasks.err.deleteFailed'), 'error')
} catch (e) { console.error('删除失败:', e) }
@@ -556,7 +556,7 @@ try {
// 搜索防抖
let _searchTimer: ReturnType<typeof setTimeout> | null = null
watch(searchKeyword, () => {
// WK-10:登记当前关键字到 barrel(数据变更联动刷新复用同一筛选,不冲掉搜索结果)
// 登记当前关键字到 barrel(数据变更联动刷新复用同一筛选,不冲掉搜索结果)
store.setActiveTaskKeyword(searchKeyword.value.trim() || undefined)
if (_searchTimer) clearTimeout(_searchTimer)
_searchTimer = setTimeout(() => {
@@ -572,18 +572,18 @@ const newTaskTitle = ref('')
const newTaskDesc = ref('')
const newTaskBranch = ref('')
const newTaskPriority = ref(2)
// F-260805:父任务下拉选中值(空串=无/顶层任务)
// 父任务下拉选中值(空串=无/顶层任务)
const newTaskParentId = ref('')
// F-260619-01:关联灵感(空串=不关联,与后端 idea_id 空串处理一致)
// 关联灵感(空串=不关联,与后端 idea_id 空串处理一致)
const newTaskIdeaId = ref('')
const submitting = ref(false)
// F-260805:父任务选项 = 当前选中项目的顶层任务(无 parent_id;仅顶层可作父,1 级嵌套)
// 父任务选项 = 当前选中项目的顶层任务(无 parent_id;仅顶层可作父,1 级嵌套)
const parentTaskOptions = computed(() =>
store.tasks.filter(t => !t.parent_id && t.project_id === newTaskProjectId.value),
)
// F-260805:树形行结构(父任务 + 直接子任务 + 子进度)。1 级嵌套,无递归。
// 树形行结构(父任务 + 直接子任务 + 子进度)。1 级嵌套,无递归。
interface TaskRow {
task: TaskRecord
/** 父任务的直接子任务(仅父有;叶子为空数组) */
@@ -615,7 +615,7 @@ interface TaskGroup {
rows: TaskRow[]
}
// F-260805:树组装 — 顶层 = 无 parent_id 的任务,每个顶层挂 children;父进度前端计算(D6,全量已在前端)。
// 树组装 — 顶层 = 无 parent_id 的任务,每个顶层挂 children;父进度前端计算(D6,全量已在前端)。
const taskRows = computed<TaskRow[]>(() => {
// 子任务按 parent_id 分组(1 级嵌套)
const childMap = new Map<string, TaskRecord[]>()
@@ -806,7 +806,7 @@ function buildTaskQuery(): TaskQuery | undefined {
return query
}
// F-260805:打开新建弹窗。可选 parent(父任务快捷菜单「添加子任务」):
// 打开新建弹窗。可选 parent(父任务快捷菜单「添加子任务」):
// 有父 → project_id 锁定为该父所属项目、parent_id 预填;无父(顶部「新建任务」)→ 默认父=无(顶层任务)。
function openCreateModal(parent?: TaskRecord) {
if (parent) {
@@ -835,9 +835,9 @@ async function confirmCreate() {
description: newTaskDesc.value.trim(),
branch_name: newTaskBranch.value.trim() || undefined,
priority: newTaskPriority.value,
// F-260619-01:空串=不关联(对齐后端 idea_id 空串语义)
// 空串=不关联(对齐后端 idea_id 空串语义)
idea_id: newTaskIdeaId.value.trim() || undefined,
// F-260805:父任务透传(空串=顶层任务,后端视为 None)
// 父任务透传(空串=顶层任务,后端视为 None)
parent_id: newTaskParentId.value || undefined,
})
if (!r) return
@@ -912,7 +912,7 @@ onMounted(async () => {
store.loadProjects(),
// F-260805 D5:一次拉当前筛选全量(limit 500),树形需要完整父子关系
store.loadTasks(buildTaskQuery()),
// F-260619-01:加载灵感供新建任务模态关联下拉选择(幂等,已加载则 no-op)
// 加载灵感供新建任务模态关联下拉选择(幂等,已加载则 no-op)
store.loadIdeas(),
])
} finally {
@@ -1171,7 +1171,7 @@ onUnmounted(() => {
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg);
/* WK-9:原 overflow:hidden 会把快捷菜单(.quick-menu 绝对定位向下溢出)裁切。
/* 原 overflow:hidden 会把快捷菜单(.quick-menu 绝对定位向下溢出)裁切。
改为组自身不裁切,圆角裁剪下放给 header(顶角)与末行(底角,见下),菜单可正常浮出。 */
}
@@ -1184,7 +1184,7 @@ onUnmounted(() => {
user-select: none;
border-bottom: 0.5px solid var(--df-border);
transition: background 0.1s;
/* WK-9:承接原 .task-group overflow:hidden 的顶角圆角裁剪(自身 overflow,组不再裁切快捷菜单) */
/* 承接原 .task-group overflow:hidden 的顶角圆角裁剪(自身 overflow,组不再裁切快捷菜单) */
border-radius: var(--df-radius-lg) var(--df-radius-lg) 0 0;
overflow: hidden;
}
@@ -1216,7 +1216,7 @@ onUnmounted(() => {
cursor: pointer;
}
.task-item:last-child { border-bottom: none; }
/* WK-9:末行承接组底角圆角裁剪(替代原 .task-group overflow:hidden) */
/* 末行承接组底角圆角裁剪(替代原 .task-group overflow:hidden) */
.task-item:last-child { border-radius: 0 0 var(--df-radius-lg) var(--df-radius-lg); }
.task-item:hover { background: var(--df-accent-bg); }