清理: 移除代码注释中的走查编号前缀(UX/B/SW/CR/ID/P0-P2-26xxxx,共102处)
This commit is contained in:
@@ -365,7 +365,7 @@ pub(super) async fn handle_approval_tool(
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
};
|
||||
emit_ai_chat_event(&app_handle, ev);
|
||||
// 审计:去重命中记一条(status 透传缓存来源 completed/rejected/failed,SW-260618-16;decided_by=auto_dedup),不进 pending
|
||||
// 审计:去重命中记一条(status 透传缓存来源 completed/rejected/failed,;decided_by=auto_dedup),不进 pending
|
||||
audit_tool_call(audit_repo, conv_id, &draft.id, &draft.name, &draft.args, &status, risk_level, Some(warned_content), Some("auto_dedup"), current_message_id).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1078,7 +1078,7 @@ pub async fn ai_approve(
|
||||
log_slow_lock(__lock_t534, "ai_approve:534");
|
||||
drop(session); // 释放锁后再执行
|
||||
|
||||
// B-260617-01 决策 a(方案 a2 后端分支):run_workflow 特殊处理。
|
||||
// 决策 a(方案 a2 后端分支):run_workflow 特殊处理。
|
||||
// run_workflow handler(tool_registry.rs:544)仅持 db:Arc<Database>,无 AppHandle/State
|
||||
// (registry/event_bus/workflows Repo),经 ai_tools.execute 调用必然 Err。原行为:审批通过 →
|
||||
// handler Err → tool_result=错误提示 → LLM 重试 → 又进审批 → 死循环(工具未标 no_retry)。
|
||||
@@ -1144,7 +1144,7 @@ pub async fn ai_approve(
|
||||
// 前端 store listen 刷新列表(仅命中映射的工具 emit,见 emit_data_changed)。
|
||||
if exec_result.is_ok() {
|
||||
emit_data_changed(&app, &approval.tool_name);
|
||||
// MED-1(CR-260620-01 审查): path 绑定工具(Medium)审批执行后 reload 白名单。
|
||||
// MED-1(审查): path 绑定工具(Medium)审批执行后 reload 白名单。
|
||||
// bind_dir_to_project / repo.update_field 直写 DB 绕过 project.rs IPC reload,
|
||||
// 此处单点补全 → AI 绑定目录即时进白名单,防后续文件工具误弹窗。
|
||||
if matches!(approval.tool_name.as_str(), "bind_directory" | "create_project" | "update_project") {
|
||||
@@ -1486,7 +1486,7 @@ pub async fn ai_authorize_dir(
|
||||
audit_finalize(&state, &tool_call_id, audit_status, Some(result_val.to_string())).await;
|
||||
if exec_result.is_ok() {
|
||||
emit_data_changed(&app, &approval.tool_name);
|
||||
// MED-1(CR-260620-01 审查): path 绑定工具(Medium)审批执行后 reload 白名单。
|
||||
// MED-1(审查): path 绑定工具(Medium)审批执行后 reload 白名单。
|
||||
// bind_dir_to_project / repo.update_field 直写 DB 绕过 project.rs IPC reload,
|
||||
// 此处单点补全 → AI 绑定目录即时进白名单,防后续文件工具误弹窗。
|
||||
if matches!(approval.tool_name.as_str(), "bind_directory" | "create_project" | "update_project") {
|
||||
@@ -2320,7 +2320,7 @@ pub async fn ai_chat_stop(
|
||||
log_slow_lock(__lock_t1611, "ai_chat_stop:1611");
|
||||
drop(session);
|
||||
|
||||
// B-260615-13 兜底任务(F2 加固,2026-08-05):loop 若 panic/异常退出漏发收尾,stop_flag 无人读,
|
||||
// 兜底任务(F2 加固,2026-08-05):loop 若 panic/异常退出漏发收尾,stop_flag 无人读,
|
||||
// 用户点 stop 无反应、generating 卡 true。
|
||||
// 原实现 sleep 3s 后仍 active 就强制复位 + emit AiCompleted——但用户点停时 loop 可能在长工具执行
|
||||
// (>3s),原兜底误触发 → conv 假完成但旧 loop 仍活 → 双完成 + 用户可再发消息(竞态 F2)。
|
||||
|
||||
@@ -180,7 +180,7 @@ pub async fn ai_conversation_create(
|
||||
// 后台跑着的 conv。决策 e:旧 conv 的 per_conv 完整保留(后台 loop 继续跑自己的 conv),
|
||||
// 仅切换 active_conversation_id 到新会话 + 为新会话建独立 per_conv。
|
||||
//
|
||||
// B-260618-06 保留:切换 active 前先持久化旧 active conv(防其内存 messages 因后续操作丢失)。
|
||||
// 保留:切换 active 前先持久化旧 active conv(防其内存 messages 因后续操作丢失)。
|
||||
// 注意:旧 conv 若有后台 loop 在跑,loop 自身的 save_conversation 也会持久化,此处 save 幂等。
|
||||
let old_conv = {
|
||||
let session = state.ai_session.lock().await;
|
||||
@@ -317,7 +317,7 @@ pub async fn ai_conversation_switch(
|
||||
None
|
||||
};
|
||||
let title = record.title.clone();
|
||||
// B-260617-17 续:历史会话 title 空(显"新对话")→ 切入后触发重新生成(用户诉求)。
|
||||
// 续:历史会话 title 空(显"新对话")→ 切入后触发重新生成(用户诉求)。
|
||||
// 含 "新对话" 占位(Some 但未生成):title.rs ensure :40 同步排除"新对话"占位不跳过,
|
||||
// 否则 title 落"新对话"(LLM 返空兜底/默认)后 is_some()=true 永远跳过 → 标题卡死。
|
||||
let need_title_regen = record.title.is_none() || record.title.as_deref() == Some("新对话");
|
||||
|
||||
@@ -497,7 +497,7 @@ pub struct ConvSummary {
|
||||
///
|
||||
/// 优先级:`pending_approvals` 非空(有挂起审批优先报 AwaitingApproval,
|
||||
/// 即使同时在流式)> `generating`(Streaming)> 都不满足(Idle)。
|
||||
// SW-260618-21 b:原预留读视图(0 消费者)。path_auth 审批链阶段1 接入后有消费者
|
||||
// b:原预留读视图(0 消费者)。path_auth 审批链阶段1 接入后有消费者
|
||||
// (try_continue_agent_loop / ai_chat_stop 经 session_state 替代手写 has_pending),不再 dead_code。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SessionState {
|
||||
@@ -1142,7 +1142,7 @@ pub enum ApprovalKind {
|
||||
Path(PathAuthRequest),
|
||||
/// 普通 RiskLevel 审批(Med/High 风险工具)。
|
||||
/// diff 仅 write_file 挂起审批前预读注入;其余工具 / recovered 审批为 None。
|
||||
#[allow(dead_code)] // IPC 活跃(useAiEvents:252+ToolCard·UX-260618-06)·cargo Rust never-read 误报
|
||||
#[allow(dead_code)] // IPC 活跃(useAiEvents:252+ToolCard·)·cargo Rust never-read 误报
|
||||
Risk { diff: Option<String> },
|
||||
}
|
||||
|
||||
@@ -1166,7 +1166,7 @@ pub(crate) struct ToolCallDraft {
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// B-260617-01 决策 a(方案 a2):run_workflow 工具经审批后的真实执行入口
|
||||
// 决策 a(方案 a2):run_workflow 工具经审批后的真实执行入口
|
||||
//
|
||||
// 背景:run_workflow handler(tool_registry.rs)仅持 db:Arc<Database>,无 AppHandle/State
|
||||
// (registry/event_bus/workflows Repo),经 ai_tools.execute 必然 Err。ai_approve 识别 run_workflow
|
||||
|
||||
@@ -167,7 +167,7 @@ fn flush_delta(app_handle: &AppHandle, conv_id: &str, pending_delta: &mut String
|
||||
/// 流式接收 LLM 响应。
|
||||
///
|
||||
/// 三类异常处理(返回 StreamResult 显式区分出口):
|
||||
/// - Init 失败(provider.stream() Err):UX-260618-15 **不 emit AiError**,返回
|
||||
/// - Init 失败(provider.stream() Err):**不 emit AiError**,返回
|
||||
/// InitFailed{retryable, error}。retryable 据状态码分类(retry::is_status_retryable 镜像):
|
||||
/// 5xx/429/timeout/connect=true,4xx(非429)/鉴权/参数错=false Fatal。调用方走流前重试(仅
|
||||
/// retryable=true,≤max_retries 次, 复用 retry::backoff_delay 退避 + 30s 总预算),重试耗尽或
|
||||
@@ -177,7 +177,7 @@ fn flush_delta(app_handle: &AppHandle, conv_id: &str, pending_delta: &mut String
|
||||
/// + 系统提示网络中断。空文本仍返回 InitFailed{retryable=true}(无文可保,交重试,emit 权归调用方)。
|
||||
/// - 用户停止(stop_flag):返回 Complete(incomplete 语义非异常中断)。
|
||||
///
|
||||
/// 心跳与停止响应(B-260615-02 / B-260615-04):单 `tokio::select!` 三分支
|
||||
/// 心跳与停止响应(/ ):单 `tokio::select!` 三分支
|
||||
/// - `stream.next()`:正常 chunk 处理
|
||||
/// - `heartbeat.tick()`(30s):静默期(如工具执行后等下一轮首 chunk)emit `AiHeartbeat`,
|
||||
/// 前端 watchdog 据此 reset,区分「LLM 在跑」与「真断」(避免空气泡误报中断)
|
||||
|
||||
@@ -88,7 +88,7 @@ pub(crate) async fn ensure_conversation_title(
|
||||
// 在传给 LLM 前合并连续同 role(拼接 content),不影响其他逻辑(本函数 summary 派生数据)。
|
||||
let summary_msgs = merge_consecutive_roles(summary_msgs);
|
||||
|
||||
// B-260617-17 修复:先用 extract 兜底 emit 让侧栏即时更新(不落库),
|
||||
// 修复:先用 extract 兜底 emit 让侧栏即时更新(不落库),
|
||||
// 下方 LLM 生成成功后覆盖。这样 LLM 失败时有重试机会,
|
||||
// 不会永久锁定一个机械截取的差标题。
|
||||
let _fallback_title = extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string());
|
||||
|
||||
@@ -73,7 +73,7 @@ pub async fn create_idea(
|
||||
state: State<'_, AppState>,
|
||||
input: CreateIdeaInput,
|
||||
) -> Result<IdeaRecord, String> {
|
||||
// priority 值域校验 ∈ 0..=3 (对标 tasks B-260615-15)。
|
||||
// priority 值域校验 ∈ 0..=3 (对标 tasks )。
|
||||
// priority_from_i32 对越界值兜底归并(>=3→Critical),但 IPC 入口应显式拒非法值,
|
||||
// 防 LLM/前端传 99 等被静默吞为 Critical。
|
||||
if !(0..=3).contains(&input.priority) {
|
||||
|
||||
@@ -338,7 +338,7 @@ pub async fn get_project(
|
||||
|
||||
/// 更新项目单个字段(字段名走 df-storage 白名单校验)
|
||||
///
|
||||
/// B-260801-010 行(id 不存在 / 已软删)→ 返 Err 而非假成功。原实现无视
|
||||
/// 行(id 不存在 / 已软删)→ 返 Err 而非假成功。原实现无视
|
||||
/// update_field 返回值恒返 Ok(true),前端误以为成功但库无变更(UI/DB 不一致)。
|
||||
///
|
||||
/// 返回值保持 bool(成功恒 true):前端 api 包装 `Promise<boolean>` 契约不变零破坏,
|
||||
|
||||
@@ -365,7 +365,7 @@ pub async fn create_task(
|
||||
|
||||
/// 更新任务单个字段(字段名走 df-storage 白名单校验;status 值走枚举校验)。
|
||||
///
|
||||
/// B-260801-010 行(id 不存在 / 已软删)→ 返 Err 而非 false,杜绝「假成功」
|
||||
/// 行(id 不存在 / 已软删)→ 返 Err 而非 false,杜绝「假成功」
|
||||
/// (前端 updateTask 仅 await 不读 bool,但 store.runWithCatch 把 Err 转 toast,用户能感知;
|
||||
/// 若返 false 则 store 静默忽略,本地状态前端手动改但库无变更,造成 UI 与 DB 不一致)。
|
||||
///
|
||||
@@ -406,7 +406,7 @@ pub async fn update_task(
|
||||
}
|
||||
}
|
||||
}
|
||||
// project_id 跨表存在性校验(B-260616-16 收尾)。
|
||||
// project_id 跨表存在性校验(收尾)。
|
||||
// crud.rs tasks 白名单保留 project_id(支持跨项目移动),跨表约束由本命令层兜底。
|
||||
// 拦截移动到不存在的项目(手输脏 id / 已物理删除的项目),避免 tasks.project_id 悬空。
|
||||
if field == "project_id" {
|
||||
|
||||
@@ -109,7 +109,7 @@ pub async fn run_workflow(
|
||||
run_workflow_inner(&app, &state, name, dag, config, task_id, target_status, "manual", None).await
|
||||
}
|
||||
|
||||
/// 工作流执行核心逻辑(B-260617-01 抽取,命令层瘦转发)。
|
||||
/// 工作流执行核心逻辑(抽取,命令层瘦转发)。
|
||||
///
|
||||
/// 原 `run_workflow` 命令体拆出,使 ai_approve 经 execute_run_workflow_for_tool 直接调用
|
||||
/// (持 &AppState 而非 tauri::State,绕开 tauri::State 在非命令上下文不可构造的限制)。
|
||||
@@ -249,7 +249,7 @@ pub async fn run_workflow_inner(
|
||||
// broadcast 不暴露被丢事件的具体类型,无法在此精确判断是否为关键终态事件。
|
||||
// 风险:若关键终态事件(WorkflowCompleted/WorkflowFailed)被丢,finished 判定不触发,
|
||||
// 循环不会 break,前端永久收不到工作流结束信号。
|
||||
// B-260615-35 兜底:收到 Lagged 时,累计次数或单次 n 达阈值则查 DB 终态,
|
||||
// 兜底:收到 Lagged 时,累计次数或单次 n 达阈值则查 DB 终态,
|
||||
// 若工作流已终态则补发对应 workflow-event 并 break 退出 forward。
|
||||
// 避免依赖实时性差的 DB 轮询兜底导致前端审批/完成弹窗卡死。
|
||||
Err(RecvError::Lagged(n)) => {
|
||||
|
||||
@@ -156,12 +156,12 @@ pub struct AppState {
|
||||
pub project_modules: ProjectModuleRepo,
|
||||
/// 工程依赖关系 Repo(V35 module_dependencies,依赖图边数据)。
|
||||
pub module_dependencies: ModuleDependencyRepo,
|
||||
/// 发布表 Repo(预留:ReleaseRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留)
|
||||
/// 发布表 Repo(预留:ReleaseRepo 持久化已就位·IPC/逻辑未接入·b 保留)
|
||||
#[allow(dead_code)]
|
||||
pub releases: ReleaseRepo,
|
||||
/// 工作流执行表 Repo
|
||||
pub workflows: WorkflowRepo,
|
||||
/// 节点执行表 Repo(预留:NodeExecutionRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留)
|
||||
/// 节点执行表 Repo(预留:NodeExecutionRepo 持久化已就位·IPC/逻辑未接入·b 保留)
|
||||
#[allow(dead_code)]
|
||||
pub node_executions: NodeExecutionRepo,
|
||||
/// 工作流事件总线(tokio broadcast)
|
||||
|
||||
+2
-2
@@ -350,10 +350,10 @@ export type Modality = 'text' | 'vision' | 'audio' | 'video'
|
||||
/** 维度 2 — 能力(模型能做什么)。serde snake_case 字面量。 */
|
||||
export type Capability = 'tool_use' | 'embedding' | 'code_gen'
|
||||
|
||||
/** 维度 3 — 价格分级(成本)。serde snake_case 字面量。'free' 已删(B-260618-05 死档,预设/启发式从不赋值)。 */
|
||||
/** 维度 3 — 价格分级(成本)。serde snake_case 字面量。'free' 已删(死档,预设/启发式从不赋值)。 */
|
||||
export type CostTier = 'low' | 'medium' | 'high'
|
||||
|
||||
/** 维度 4 — 智力分级。serde snake_case 字面量。CR-260618-11#3:与 CostTier 同,cost/intel 已从路由硬过滤及前端标签去除,字段保留仅 DB 兼容,不再参与路由选模型。 */
|
||||
/** 维度 4 — 智力分级。serde snake_case 字面量。与 CostTier 同,cost/intel 已从路由硬过滤及前端标签去除,字段保留仅 DB 兼容,不再参与路由选模型。 */
|
||||
export type IntelligenceTier = 'lite' | 'standard' | 'plus' | 'ultra'
|
||||
|
||||
/** 探测来源(只读,由后端探测器填充)。serde snake_case 字面量。 */
|
||||
|
||||
@@ -265,7 +265,7 @@ async function confirmClearChat() {
|
||||
* 同属真并发语义——不弹「中断并新建」确认(与并行矛盾)。对齐 switchConversation:89-91
|
||||
* 「允许生成中切换:后台继续生成,事件按 conversation_id 路由」。
|
||||
*
|
||||
* 历史:B-260615-10 单 loop 软复位弹中断确认,F-09 决策e 上线后已废弃(A 路线遗留)。
|
||||
* 历史:单 loop 软复位弹中断确认,F-09 决策e 上线后已废弃(A 路线遗留)。
|
||||
*/
|
||||
async function confirmNewConversation() {
|
||||
try {
|
||||
@@ -476,7 +476,7 @@ onBeforeUnmount(() => {
|
||||
store.stopListener()
|
||||
// F-15 阶段2: 释放上下文管理事件监听器
|
||||
store.stopContextListener()
|
||||
if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } // B-260616-12
|
||||
if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } //
|
||||
if (_unlistenAutoApproved) { _unlistenAutoApproved(); _unlistenAutoApproved = null } // AE-2025-04
|
||||
if (_unlistenAiToast) { _unlistenAiToast(); _unlistenAiToast = null } // P1-e-d
|
||||
if (_unlistenSkillInjected) { _unlistenSkillInjected(); _unlistenSkillInjected = null } // 技能注入反馈 toast
|
||||
@@ -802,7 +802,7 @@ async function initContextEventListeners(): Promise<void> {
|
||||
}
|
||||
.ai-queue-x { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0; }
|
||||
.ai-queue-x:hover { color: var(--df-danger); }
|
||||
/* UX-260616-06 队列项编辑/立即发送按钮 + 编辑态 input */
|
||||
/* 队列项编辑/立即发送按钮 + 编辑态 input */
|
||||
.ai-queue-btn {
|
||||
background: none; border: none; color: var(--df-text-dim); cursor: pointer;
|
||||
font-size: 11px; line-height: 1; padding: 0 4px; flex-shrink: 0;
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
@result="answerConfirm"
|
||||
/>
|
||||
|
||||
<!-- UX-260617-14:审批 loading 超时兜底 toast(零依赖 reactive,复用 AiChat 同款样式) -->
|
||||
<!-- 审批 loading 超时兜底 toast(零依赖 reactive,复用 AiChat 同款样式) -->
|
||||
<Transition name="ai-tool-toast">
|
||||
<div v-if="approveToast.visible" class="ai-tool-toast ai-tool-toast--warning">{{ approveToast.msg }}</div>
|
||||
</Transition>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
@expand-content="toggleExpand"
|
||||
@approve="(e) => emit('approve', e)"
|
||||
@install-obscura="() => emit('install-obscura')"
|
||||
:class="{ 'ai-tool-card--group-hidden': group.calls.length >= 2 && isGroupCollapsed(gi) }"
|
||||
:class="{ 'ai-tool-card--group-hidden': isGroupHidden(tc, group, gi) }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@@ -123,19 +123,36 @@ const pendingCount = computed(() =>
|
||||
* 响应式而非一次性初始化——流式场景第一张卡到达即触发旧一次性逻辑,
|
||||
* 此后组从 1 卡长到 N 卡不再折叠,批量读取的 N 张单行卡全展开刷屏;
|
||||
* 签名(组名:卡数)变化即幂等补齐,新达到阈值的组收起,已 touched 的组不动。
|
||||
*
|
||||
* 活组豁免:含 running/pending_approval 卡或用户展开过卡的组(groupHasLiveCard)
|
||||
* 不自动收;已被收的活组自动复活展开。治"执行轮整组(读取文件/搜索内容)被收只剩组头,
|
||||
* running 卡强制展开也被组 hidden 压住,用户想看进度要点两层"。
|
||||
*/
|
||||
watch(
|
||||
() => groupedToolCalls.value.map(g => `${g.name}:${g.calls.length}`).join('|'),
|
||||
() => {
|
||||
const next = new Set(collapsedGroups.value)
|
||||
groupedToolCalls.value.forEach((g, gi) => {
|
||||
if (g.calls.length >= 2 && !userTouchedGroups.value.has(g.name)) next.add(gi)
|
||||
if (g.calls.length < 2 || userTouchedGroups.value.has(g.name)) return
|
||||
if (groupHasLiveCard(g)) next.delete(gi)
|
||||
else next.add(gi)
|
||||
})
|
||||
collapsedGroups.value = next
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/** 组内是否存在"活着"的卡:running/pending_approval 进行中 或 用户主动展开过(userExpandedCards)。
|
||||
* 自动收起补齐对这类组跳过(不误收用户正在看的/正在执行的),
|
||||
* 且补齐时自动展开复活——治"组从 1 卡长到 2 卡瞬间整组被收、用户点开也被压回去"。 */
|
||||
function groupHasLiveCard(g: ToolCallGroup): boolean {
|
||||
return g.calls.some(tc =>
|
||||
tc.status === 'running'
|
||||
|| tc.status === 'pending_approval'
|
||||
|| userExpandedCards.value.has(tc.id)
|
||||
)
|
||||
}
|
||||
|
||||
function isGroupCollapsed(gi: number): boolean {
|
||||
return collapsedGroups.value.has(gi)
|
||||
}
|
||||
@@ -180,7 +197,9 @@ function groupDisplayName(group: ToolCallGroup): string {
|
||||
|
||||
/**
|
||||
* 单卡是否展开:
|
||||
* - 分组已折叠 → false(整组隐藏)
|
||||
* - 分组已折叠 → false(整组隐藏)。**例外**: 用户主动展开过的卡(userExpandedCards)
|
||||
* 不被组折叠压制()——组收起只隐藏"无用户记忆"的卡,
|
||||
* 用户点开过的卡保持展开,否则"点开又被组补齐自动压回去"死循环。
|
||||
* - 未手动操作过 → 默认折叠(U-260629):completed 卡一律单行 header(工具名+目标+结果摘要),
|
||||
* 杜绝"AI 一轮多工具各铺开一整卡"刷屏;点 header 才展开。
|
||||
* running/pending/rejected/write_file 不受此值影响——ToolCard 内部
|
||||
@@ -189,17 +208,26 @@ function groupDisplayName(group: ToolCallGroup): string {
|
||||
* 清了 expandedCards,记忆态仍保持展开(用户读过的卡不强制折叠)。
|
||||
*/
|
||||
function isCardExpanded(tc: AiToolCallInfo, gi: number): boolean {
|
||||
if (isGroupCollapsed(gi)) return false
|
||||
// 用户主动折叠最优先(覆盖用户展开记忆)
|
||||
// 用户主动折叠最优先(覆盖一切,含组折叠与展开记忆)
|
||||
if (userCollapsedCards.value.has(tc.id)) return false
|
||||
// 用户主动展开的卡片记忆态优先(不被自动收起影响)
|
||||
// 用户主动展开记忆次优先:压过组折叠(),点开过的卡组收起时仍展开
|
||||
if (userExpandedCards.value.has(tc.id)) return true
|
||||
// 组折叠(只影响无用户记忆的卡)
|
||||
if (isGroupCollapsed(gi)) return false
|
||||
// 用户手动切换过该卡(当前态)
|
||||
if (expandedCards.value.has(tc.id)) return true
|
||||
// 兜底:默认折叠(单行 header 已有 toolDisplayName 路径/pattern/host + toolResultSummary 摘要)
|
||||
return false
|
||||
}
|
||||
|
||||
/** 组收起时卡片是否整卡隐藏(display:none)。
|
||||
* 用户主动展开过的卡豁免——组收起只隐藏无用户记忆的卡,
|
||||
* 否则模板 class 的 display:none 会把 isCardExpanded 已放行的展开卡重新藏掉。 */
|
||||
function isGroupHidden(tc: AiToolCallInfo, group: ToolCallGroup, gi: number): boolean {
|
||||
if (group.calls.length < 2 || !isGroupCollapsed(gi)) return false
|
||||
return !userExpandedCards.value.has(tc.id)
|
||||
}
|
||||
|
||||
function toggleCardExpand(tc: AiToolCallInfo, gi: number) {
|
||||
const id = tc.id
|
||||
// 用 isCardExpanded 判当前真实态(含用户操作记忆),而非 expandedCards(记忆态不在其中,
|
||||
@@ -399,7 +427,7 @@ function groupIcon(name: string): string {
|
||||
}
|
||||
|
||||
/* -- 分组隐藏态的卡片。
|
||||
UX-260616-03: 保持 display:none(原行为)。
|
||||
保持 display:none(原行为)。
|
||||
不改 max-height/opacity 动画的原因:flex 容器 gap:8px 对零高 item 仍贡献 gap,
|
||||
会留双倍间距致布局回归;display:none 移出流才无 gap。
|
||||
真正的高度动画需 <Transition> JS 钩子(高度 auto→0 不可纯 CSS transition),
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
</div>
|
||||
|
||||
<!-- patch_file 结果:文件路径/字节增减 + unified diff 红删绿增行级预览(复用 ai-tool-diff-pre/--add/--del/--ctx 既有样式)
|
||||
UX-260618-06:后端 generate_diff 产完整 diff,result.diff 字段此前被丢弃,用户无法验证改了什么 → 此分支渲染 diff -->
|
||||
后端 generate_diff 产完整 diff,result.diff 字段此前被丢弃,用户无法验证改了什么 → 此分支渲染 diff -->
|
||||
<div v-else-if="tc.name === 'patch_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-patch-result">
|
||||
<div class="ai-tool-file-bar">
|
||||
<span class="ai-tool-file-icon" v-html="fileIcon"></span>
|
||||
@@ -153,7 +153,7 @@
|
||||
<pre v-if="cmdOutputExpanded && cmdOutput" class="ai-tool-cmd-output"><code>{{ cmdOutput }}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- http_request 结果:method + url + 状态码徽标 + 可折叠响应 body(对齐 run_command 模式 UX-260618-08) -->
|
||||
<!-- http_request 结果:method + url + 状态码徽标 + 可折叠响应 body(对齐 run_command 模式 ) -->
|
||||
<div
|
||||
v-else-if="tc.name === 'http_request' && tc.result && tc.status === 'completed' && parsed"
|
||||
class="ai-tool-http-result"
|
||||
|
||||
@@ -279,7 +279,7 @@ watch(() => store.state.streaming, (s) => {
|
||||
// 收起旧的已完成/已拒绝卡片,保留 running/pending/write_file 活跃卡片。
|
||||
// 折叠态由 ToolCardList 自治,父级仅经 expose 调 collapseInactive ──
|
||||
//
|
||||
// B-260616-13 性能:快照仅含 toolCalls 的 (id,status)+消息条数,剔除 content.length。
|
||||
// 性能:快照仅含 toolCalls 的 (id,status)+消息条数,剔除 content.length。
|
||||
// 收起判定只依赖 active Set(由 toolCalls 状态推导),与文本长度无关。
|
||||
// 原快照含 len → 流式每 token 增长末条消息 content → snap 变 → body 每帧重算
|
||||
// (deep watch 检测仍每帧跑,但 body 跳过)。剔除 len 后流式 delta 不再触发收起重算,
|
||||
@@ -1049,7 +1049,7 @@ defineExpose({
|
||||
正常消息 + 展开的折叠段内消息 emit 'msg' 项。连续 archived_segment/compressed
|
||||
合并一条分隔条,点击展开/收起看原文)
|
||||
A 过程压缩:trace 项=完成轮次执行轨迹卡,step 项=执行中实时步骤摘要,均先于 msg 分支拦截。
|
||||
UX-2025-19: 每项外层包 .ai-msg-slot(B-260618-01 虚拟滚动已移除,现恒渲染;
|
||||
每项外层包 .ai-msg-slot(虚拟滚动已移除,现恒渲染;
|
||||
slot 仅作 flex 子占位 + 消息分组的语义容器,gap 由 .ai-messages 承担)。
|
||||
UX 密度改造:timesep 项=相邻消息间隔 ≥30 分钟或跨天的居中时间分隔线。 -->
|
||||
<template v-for="item in renderItems" :key="item.key">
|
||||
@@ -1119,7 +1119,7 @@ defineExpose({
|
||||
<span class="ai-retry-text">{{ item.msg.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- B-260618-19: 纯空 assistant(无 content/error/toolCalls 且非末条流式)整 slot 不渲染,
|
||||
<!-- 纯空 assistant(无 content/error/toolCalls 且非末条流式)整 slot 不渲染,
|
||||
消除空 div + flex gap 堆积的大片空白;sep 始终渲染;有 toolCalls 的空 assistant
|
||||
仍渲染(工具卡可见——修复 agentic 工具轮 LLM 无文本时工具卡消失)。
|
||||
UX 密度改造:per-block 相对时间已移除,精确时刻上移到消息容器 title(hover 可查)。 -->
|
||||
@@ -1152,8 +1152,8 @@ defineExpose({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息(正常段 + 展开的折叠段内消息)— 虚拟滚动已移除(UX-260617-01:IO/RO 时序致重叠),消息恒渲染 -->
|
||||
<!-- B-260618-19: 纯空 assistant 过滤已上提到外层 .ai-msg-slot 的 v-if(shouldRenderMsg),
|
||||
<!-- 消息(正常段 + 展开的折叠段内消息)— 虚拟滚动已移除(IO/RO 时序致重叠),消息恒渲染 -->
|
||||
<!-- 纯空 assistant 过滤已上提到外层 .ai-msg-slot 的 v-if(shouldRenderMsg),
|
||||
此处 v-else 恒渲染 .ai-msg(sep 已在上方 v-if 分支处理,此处必为 msg)。 -->
|
||||
<div
|
||||
v-else
|
||||
@@ -1237,7 +1237,7 @@ defineExpose({
|
||||
<span class="ai-cursor"></span>
|
||||
</div>
|
||||
|
||||
<!-- UX-03 / UX-260616-01:错误气泡操作入口(重试 / 去设置) -->
|
||||
<!-- UX-03 / 错误气泡操作入口(重试 / 去设置) -->
|
||||
<div v-if="item.msg.isError" class="ai-msg-actions ai-msg-actions--error">
|
||||
<span v-if="!canRetry(item.msg)" class="ai-msg-not-retryable">{{ $t('ai.notRetryable') }}</span>
|
||||
<button
|
||||
@@ -1341,7 +1341,7 @@ defineExpose({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.ai-msg-slot(B-260618-01:虚拟滚动已移除,恒渲染容器) -->
|
||||
</div><!-- /.ai-msg-slot(虚拟滚动已移除,恒渲染容器) -->
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1468,7 +1468,7 @@ defineExpose({
|
||||
color: var(--df-danger);
|
||||
border-color: rgba(240,101,101,0.3);
|
||||
}
|
||||
/* 修正 UX-260617-21 的 display:block 方案——display:block 让 table 失去表格
|
||||
/* 修正 的 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 下
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class="status-tag" :class="'status-' + idea.status">{{ $t(ideaStatusLabelKey(idea.status)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- B-260615-25:灵感描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail),空值回退 — -->
|
||||
<!-- 灵感描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail),空值回退 — -->
|
||||
<template v-if="editing">
|
||||
<textarea v-model="editDesc" class="detail-desc-edit" rows="4"></textarea>
|
||||
<div class="desc-edit-actions">
|
||||
@@ -725,7 +725,7 @@ watch(() => props.idea.id, loadHistory)
|
||||
margin-bottom: var(--df-gap-page);
|
||||
}
|
||||
|
||||
/* ===== 灵感描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */
|
||||
/* ===== 灵感描述 Markdown 渲染(,基础样式收敛至全局 ai-md.css) ===== */
|
||||
.detail-desc.ai-md { white-space: normal; color: var(--df-text-secondary); }
|
||||
|
||||
.detail-section {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div class="detail-field">
|
||||
<label>{{ t('knowledge.contentLabel') }}</label>
|
||||
<!-- B-260615-25:知识内容 Markdown 渲染(展示态),复用 useMarkdown composable(同 B-24 TaskDetail);编辑态保持 textarea -->
|
||||
<!-- 知识内容 Markdown 渲染(展示态),复用 useMarkdown composable(同 B-24 TaskDetail);编辑态保持 textarea -->
|
||||
<div
|
||||
v-if="!editing && detail.knowledge.content"
|
||||
ref="contentEl"
|
||||
@@ -150,7 +150,7 @@ import { useRendered } from '@/composables/useMarkdown'
|
||||
import { formatRelative } from '@/utils/time'
|
||||
import type { KnowledgeDetailPayload, KnowledgeEventRecord } from '@/api/types'
|
||||
|
||||
// 详情面板子组件(Knowledge.vue 拆分,SW-260618-20):
|
||||
// 详情面板子组件(Knowledge.vue 拆分,):
|
||||
// 接收 detail 渲染只读四段(基本信息/溯源/引用/时间线),内部自管 editing/editForm/refLimit;
|
||||
// 编辑保存与审核操作通过 emit 回传父级执行 IPC(publish/reject/archive/save)。
|
||||
const props = defineProps<{
|
||||
@@ -202,7 +202,7 @@ function saveEdit() {
|
||||
editing.value = false
|
||||
}
|
||||
|
||||
// ===== Markdown 渲染(B-260615-25,复用模块单例渲染器)=====
|
||||
// ===== Markdown 渲染(,复用模块单例渲染器)=====
|
||||
const { rendered: renderedContent, ensureLoaded } = useRendered(
|
||||
() => props.detail?.knowledge.content ?? '',
|
||||
)
|
||||
@@ -342,11 +342,11 @@ watch(renderedContent, () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 详情面板四段(SW-260618-20 从 Knowledge.vue 迁移,样式与原一致) */
|
||||
/* 详情面板四段(从 Knowledge.vue 迁移,样式与原一致) */
|
||||
.detail-section { padding-bottom: 18px; margin-bottom: 18px; border-bottom: 0.5px solid var(--df-border); }
|
||||
.detail-section:last-child { border-bottom: none; margin-bottom: 0; }
|
||||
|
||||
.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px; min-width: 0; }
|
||||
.detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 12px; min-width: 0; flex-wrap: wrap; }
|
||||
.detail-title-row { display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1; overflow: hidden; }
|
||||
.detail-kind-icon { font-size: 20px; flex-shrink: 0; }
|
||||
.detail-title { font-size: 18px; font-weight: 500; color: var(--df-text); word-break: break-word; overflow-wrap: break-word; min-width: 0; }
|
||||
@@ -358,7 +358,7 @@ watch(renderedContent, () => {
|
||||
.detail-field label { display: block; font-size: 12px; color: var(--df-text-secondary); margin-bottom: 4px; }
|
||||
.detail-content { font-size: 13px; color: var(--df-text); line-height: 1.6; white-space: pre-wrap; }
|
||||
|
||||
/* ===== 知识内容 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css)===== */
|
||||
/* ===== 知识内容 Markdown 渲染(,基础样式收敛至全局 ai-md.css)===== */
|
||||
.detail-content.ai-md { white-space: normal; }
|
||||
.detail-tags { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.tag { font-size: 11px; padding: 2px 8px; border-radius: var(--df-radius-xs); background: var(--df-accent-bg); color: var(--df-accent); }
|
||||
@@ -448,10 +448,18 @@ watch(renderedContent, () => {
|
||||
|
||||
/* 按钮基础样式 (.btn / .btn-sm / .btn-primary / .btn-ghost) 已收敛至 global.css */
|
||||
|
||||
/* ===== 响应式:窄屏详情标题防竖线化(B-260619)===== */
|
||||
/* ===== 响应式:窄屏详情标题防竖线化()===== */
|
||||
/* 双闸:@media 管窗口窄;@container 管「视口宽但面板被 AI 平分挤压」场景。
|
||||
注意:@container 需要 .knowledge(父链上有 container-type 的祖先)声明容器,
|
||||
已在 Knowledge.vue 根节点加 container-type: inline-size。 */
|
||||
@media (max-width: 760px) {
|
||||
.detail-head { flex-direction: column; align-items: flex-start; }
|
||||
.detail-actions { width: 100%; }
|
||||
.detail-title { font-size: 15px; }
|
||||
}
|
||||
@container (max-width: 420px) {
|
||||
.detail-head { flex-direction: column; align-items: flex-start; }
|
||||
.detail-actions { width: 100%; }
|
||||
.detail-title { font-size: 15px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<!-- 人工审批对话框(从 ProjectDetail.vue 抽离 UX-260618-17 submitting 保留)-->
|
||||
<!-- 人工审批对话框(从 ProjectDetail.vue 抽离 submitting 保留)-->
|
||||
<div v-if="visible" class="modal-overlay">
|
||||
<div class="modal-box" style="width: 500px">
|
||||
<h3 class="modal-title">{{ $t('projectDetail.approvalTitle') }}</h3>
|
||||
@@ -76,7 +76,7 @@ import { useToast } from '@/composables/useToast'
|
||||
|
||||
// 抽离自 ProjectDetail.vue(L175-225 模板 + L416-471 审批逻辑/watch)
|
||||
// store 共享:父级 useProjectStore() 实例传入(同源 store,响应式共享 pendingApproval)
|
||||
// submitting 自治:本组件持有禁用态(UX-260618-17 双击防抖)
|
||||
// submitting 自治:本组件持有禁用态(双击防抖)
|
||||
// ProjectStore 类型未 export(store.ts:135 内部 type),用 ReturnType 自推导同源类型
|
||||
type ProjectStore = ReturnType<typeof useProjectStore>
|
||||
|
||||
@@ -135,7 +135,7 @@ async function handleApproval(decision: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 多选审批(UX-260618-17 submitting 防双击保留)
|
||||
// 多选审批(submitting 防双击保留)
|
||||
async function handleApprovalMulti() {
|
||||
if (multiDecisions.value.length === 0) return
|
||||
submitting.value = true
|
||||
|
||||
@@ -71,10 +71,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片(外部 diff 存在时让位) -->
|
||||
<!-- 图片(外部 diff 存在时让位;加载失败显示错误占位,不静默空白) -->
|
||||
<div v-else-if="isImage && !isExternalDiff" class="preview-image">
|
||||
<img :src="imageUrl ?? ''" :alt="filePath ?? ''" />
|
||||
<img
|
||||
v-if="imageUrl && !imageLoadError"
|
||||
:src="imageUrl"
|
||||
:alt="filePath ?? ''"
|
||||
@error="imageLoadError = true"
|
||||
/>
|
||||
<div v-else class="preview-image-error">
|
||||
<div class="preview-placeholder-icon">🖼️</div>
|
||||
<div class="preview-placeholder-text">图片加载失败(路径: {{ filePath }})</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Diff 视图(外部注入的 diff 优先展示;置于 Markdown 之前,使 .md 文件也支持 Diff) -->
|
||||
@@ -171,6 +180,8 @@ const isBinary = ref(false)
|
||||
const fileSize = ref<number | null>(null)
|
||||
const truncated = ref(false)
|
||||
const imageUrl = ref<string | null>(null)
|
||||
/** 图片加载失败标记(asset 协议不可用/文件被删等);@error 置位,文件切换时复位 */
|
||||
const imageLoadError = ref(false)
|
||||
|
||||
/** 符号概览(fileOutline 结果;仅支持语言 + 非 diff 视图时展示)。 */
|
||||
const outlineSupported = ref(false)
|
||||
@@ -312,7 +323,7 @@ const hljsClass = computed(() => {
|
||||
})
|
||||
|
||||
/** 图片后缀白名单。 */
|
||||
const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico']
|
||||
const IMAGE_EXT = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico', '.avif']
|
||||
|
||||
/** Markdown 后缀判定。 */
|
||||
const isMarkdown = computed(() => {
|
||||
@@ -486,6 +497,7 @@ async function loadFile() {
|
||||
URL.revokeObjectURL(imageUrl.value)
|
||||
imageUrl.value = null
|
||||
}
|
||||
imageLoadError.value = false
|
||||
// 重置上一文件的符号概览(新文件结果到达后再填充)
|
||||
outlineSupported.value = false
|
||||
outlineSymbols.value = []
|
||||
|
||||
@@ -87,7 +87,7 @@ const reviewSuggestions = computed<string[]>(() => {
|
||||
<style scoped>
|
||||
/* ===== F-AiNodeSelfReview: output_json 区块(产出 + 自审结论卡) ===== */
|
||||
/* 基础布局 .info-item / .info-block / .label / .value 已收敛至全局 components.css
|
||||
(DRY 收口 B-260619)。全局类对子组件内部 DOM 生效(全局非 scoped 无渗透限制),
|
||||
(DRY 收口)。全局类对子组件内部 DOM 生效(全局非 scoped 无渗透限制),
|
||||
故本子组件无需复制等价基础布局,仅保留本组件特有的 output/review 样式。 */
|
||||
|
||||
.output-block .output-body {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! 审批链(approveToolCall/batchApprove)已拆分到 useAiApproval.ts(fe-arch P0-1)。
|
||||
//!
|
||||
//! B-260616-02 L2 发送韧性:三级降级
|
||||
//! L2 发送韧性:三级降级
|
||||
//! L0 normal:后端 idle → 直接发送
|
||||
//! L1 queued: 后端 busy → 入队等待(≤30s 正常排队)
|
||||
//! L2 force: 排队>30s → 弹 confirm → 用户确认后调 ai_chat_force_send 复位残留并强制发出
|
||||
@@ -510,7 +510,7 @@ async function stopChat() {
|
||||
flushCurrentText()
|
||||
setStreaming(false, { convId: state.activeConversationId, reason: 'stopChat' }) // 本地先复位,不依赖后端 AiCompleted(审批态看门狗已 clear,竞态丢失则卡死)
|
||||
clearStreamWatchdog() // 清残留看门狗
|
||||
// UX-260616-05 决策 a(逐条续发):不再清队列,保留供 AiCompleted 链式触发 drainQueue 续发。
|
||||
// 决策 a(逐条续发):不再清队列,保留供 AiCompleted 链式触发 drainQueue 续发。
|
||||
// 后端 agentic/mod.rs emit AiCompleted 注释明确「保证前端收事件时后端已可接下一条,队列续发不被拒」;
|
||||
// 当前未完成回复已入库(agentic/mod.rs save_conversation)保留为截断回复。
|
||||
try {
|
||||
@@ -540,8 +540,8 @@ export function useAiSend() {
|
||||
drainQueue,
|
||||
cancelQueued,
|
||||
clearQueue,
|
||||
editQueued, // UX-260616-06
|
||||
sendQueuedNow, // UX-260616-07
|
||||
editQueued, //
|
||||
sendQueuedNow, //
|
||||
stopChat,
|
||||
isQueueTimedOut,
|
||||
tryForceSend,
|
||||
|
||||
@@ -188,7 +188,7 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
|
||||
|
||||
/**
|
||||
* 拒绝按钮处理(risk 类专用)。
|
||||
* loading + 超时兜底复用 B-260616-08 机制。拒绝不做 High 风险二次确认(无害,加确认反增摩擦)。
|
||||
* loading + 超时兜底复用 机制。拒绝不做 High 风险二次确认(无害,加确认反增摩擦)。
|
||||
*/
|
||||
async function onReject() {
|
||||
if (approving.value) return // 防重入
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface ToolResult {
|
||||
has_more?: boolean
|
||||
warning?: string
|
||||
review_rounds?: number
|
||||
// UX-260618-05~12:补全后端返回前端消费的字段(review_rounds/truncated/bytes_moved/has_more/total/entries 已存在)
|
||||
// ~12:补全后端返回前端消费的字段(review_rounds/truncated/bytes_moved/has_more/total/entries 已存在)
|
||||
task_id?: string
|
||||
execution_id?: string
|
||||
target_status?: string
|
||||
@@ -146,7 +146,7 @@ export function shouldKeepOpen(tc: AiToolCallInfo): boolean {
|
||||
* - 通用工具 raw 文本:AR-6 路径后端塞 serde_json::Value::String("工具 X 执行失败: ..."),
|
||||
* 前端 parseResult 返回 null(非 JSON 对象)→ 走 raw 文本兜底,无结构信号
|
||||
* 此时按行首前缀 "执行失败"/"Error:"/"Failed:" 启发式判定(对齐后端 err_msg 格式,
|
||||
* 行首锚定避免误伤含这些子串的正常产出,见 UX-260617-18)。
|
||||
* 行首锚定避免误伤含这些子串的正常产出,见 )。
|
||||
*
|
||||
* 注意:status==='rejected'(用户拒绝)走独立分支,不算工具失败,不进此函数。
|
||||
* running/pending_approval 自然 false(无 result 或 result 未稳定)。
|
||||
@@ -631,7 +631,7 @@ export function formatFetchUrl(r: ToolResult): string {
|
||||
|
||||
/** list_* summary 版:n=0 返空串(折叠态无噪音),按 name 选 i18n key */
|
||||
function summaryList(r: ToolResult, name: string): string {
|
||||
// UX-260618-10/12: 后端返 {items,total,has_more} 对象(非数组),原 Array.isArray(r) 恒 false→折叠态无计数
|
||||
// 后端返 {items,total,has_more} 对象(非数组),原 Array.isArray(r) 恒 false→折叠态无计数
|
||||
const n = Array.isArray(r) ? r.length
|
||||
: (typeof r.total === 'number' ? r.total
|
||||
: (Array.isArray(r.items) ? r.items.length : 0))
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface DiffLine {
|
||||
/**
|
||||
* 解析 unified diff 文本 → DiffLine[]。
|
||||
*
|
||||
* 来源(AE-2025-03 / UX-260618-06):后端 generate_diff 输出 '+新行/-旧行/(空格)上下文',
|
||||
* 来源(AE-2025-03 / ):后端 generate_diff 输出 '+新行/-旧行/(空格)上下文',
|
||||
* 逐行拆 → {kind, text} 供模板按 kind 着色(add 绿/del 红/ctx 灰)。
|
||||
*
|
||||
* - 空行过滤(generate_diff 末尾保留单尾换行 → split 末尾有空串元素)
|
||||
|
||||
@@ -37,7 +37,7 @@ import shell from 'highlight.js/lib/languages/shell'
|
||||
import java from 'highlight.js/lib/languages/java'
|
||||
import csharp from 'highlight.js/lib/languages/csharp'
|
||||
import cpp from 'highlight.js/lib/languages/cpp'
|
||||
// B-260616-11(②子项): diff 语言注册,```diff 代码块 +/- 行获 hljs-addition/deletion 红绿(github-dark.css 自带)
|
||||
// (②子项): diff 语言注册,```diff 代码块 +/- 行获 hljs-addition/deletion 红绿(github-dark.css 自带)
|
||||
import diff from 'highlight.js/lib/languages/diff'
|
||||
// 主题:github-dark 与现有深色 UI(#0c0e1a)贴合;亮色主题下 code 块仍可读(深底浅字)。
|
||||
// 注意:该 CSS 在 ai-md.css 之前/之后导入均可,hljs 用 .hljs/.hljs-* token class 着色,
|
||||
@@ -185,7 +185,7 @@ function renderMd(text: string): string {
|
||||
if (!mdReady.value || !_marked || !_purify) {
|
||||
return escapeFallback(text)
|
||||
}
|
||||
// B-260616-11(①子项): 裸 diff 文本预处理。渲染前将无围栏的连续 +/- 行包成 ```diff
|
||||
// (①子项): 裸 diff 文本预处理。渲染前将无围栏的连续 +/- 行包成 ```diff
|
||||
// 围栏代码块,复用②子项已注册的 diff 语言 hljs 红绿样式,零额外 CSS。
|
||||
// 防御性:即使现网 AI 多带围栏,裸行也正确处理(围栏内整段不动)。
|
||||
const wrapped = wrapNakedDiff(text)
|
||||
@@ -198,7 +198,7 @@ function renderMd(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* B-260616-11(①子项,方案A wrapNakedDiff): 裸 +/- diff 文本预处理。
|
||||
* (①子项,方案A wrapNakedDiff): 裸 +/- diff 文本预处理。
|
||||
*
|
||||
* 现网 AI(尤其 deepseek/glm/qwen 等)在描述「改动/对比」时常直接吐裸 -/+ 行而忘记
|
||||
* 围栏(或仅用普通文本),导致 marked 当成普通段落/list 渲染,丢失 diff 视觉语义。
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
// ── Pending queue ──
|
||||
queueTitle: 'Queued {n}/10',
|
||||
clearQueue: 'Clear',
|
||||
// UX-260616-06/07: queue item edit + send now
|
||||
// queue item edit + send now
|
||||
queueEdit: 'Edit',
|
||||
queueSave: 'Save',
|
||||
queueCancel: 'Cancel',
|
||||
@@ -164,7 +164,7 @@ export default {
|
||||
editNotLastUser: 'Only the last user message can be edited',
|
||||
placeholderEditing: 'Edit message then Enter to regenerate (Esc to cancel)',
|
||||
|
||||
// ── Error bubble actions (UX-03 / UX-260616-01) ──
|
||||
// ── Error bubble actions (UX-03 / ) ──
|
||||
retry: 'Retry',
|
||||
goToSettingsAction: 'Open settings',
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
statusRunning: 'Running',
|
||||
statusPending: 'Pending approval',
|
||||
statusRejected: 'Rejected',
|
||||
// UX-260618-08/11: truncation hints (read_file paged truncation / list_directory 1000-entry cap)
|
||||
// truncation hints (read_file paged truncation / list_directory 1000-entry cap)
|
||||
linesTruncated: '{shown}/{total} lines, truncated',
|
||||
dirTruncated: 'Truncated, first {n} items only',
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
taskCount: '{n} tasks',
|
||||
projectCount: '{n} projects',
|
||||
ideaCount: '{n} ideas',
|
||||
// UX-260618-10/12: list_trash collapsed header count + paged has_more hint (backend returns {items,total,has_more} object)
|
||||
// list_trash collapsed header count + paged has_more hint (backend returns {items,total,has_more} object)
|
||||
trashCount: '{n} in trash',
|
||||
listHasMore: '({n} shown, more available)',
|
||||
createdWithName: 'Created: {name}',
|
||||
|
||||
@@ -82,7 +82,7 @@ export default {
|
||||
fetchFailedNotFound: 'Endpoint not found, this vendor may not support model listing: {msg}',
|
||||
toastModelEditLocalOnly: 'Enabled / weight are local edits, overwritten on next fetch',
|
||||
|
||||
// Model trait tags (modality + capability + probe source; cost/intel 去除见 UX-260618-04)
|
||||
// Model trait tags (modality + capability + probe source; cost/intel 去除见 )
|
||||
tagModality: { text: 'Text', vision: 'Vision', audio: 'Audio', video: 'Video' },
|
||||
tagCapability: { tool_use: 'Tool use', embedding: 'Embedding', code_gen: 'Code' },
|
||||
tagSource: { user_set: 'Manual', preset_table: 'Preset', heuristic: 'Heuristic', default: 'Default' },
|
||||
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
// ── 待发送队列 ──
|
||||
queueTitle: '待发送 {n}/10',
|
||||
clearQueue: '清空',
|
||||
// UX-260616-06/07:队列项编辑 + 立即发送
|
||||
// 队列项编辑 + 立即发送
|
||||
queueEdit: '编辑',
|
||||
queueSave: '保存',
|
||||
queueCancel: '取消',
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
editNotLastUser: '只能编辑最后一条用户消息',
|
||||
placeholderEditing: '编辑消息后回车重新生成(Esc 取消)',
|
||||
|
||||
// ── 错误气泡操作入口(UX-03 / UX-260616-01) ──
|
||||
// ── 错误气泡操作入口(UX-03 / ) ──
|
||||
retry: '重试',
|
||||
goToSettingsAction: '去设置',
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
statusRunning: '进行中',
|
||||
statusPending: '待审批',
|
||||
statusRejected: '已拒绝',
|
||||
// UX-260618-08/11:截断提示(read_file 分页截断 / list_directory 1000 上限截断)
|
||||
// 截断提示(read_file 分页截断 / list_directory 1000 上限截断)
|
||||
linesTruncated: '{shown}/{total} 行,已截断',
|
||||
dirTruncated: '已截断,仅前 {n} 项',
|
||||
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
taskCount: '{n} 项任务',
|
||||
projectCount: '{n} 个项目',
|
||||
ideaCount: '{n} 条灵感',
|
||||
// UX-260618-10/12:list_trash 折叠态计数 + 分页 has_more 提示(后端返 {items,total,has_more} 对象)
|
||||
// list_trash 折叠态计数 + 分页 has_more 提示(后端返 {items,total,has_more} 对象)
|
||||
trashCount: '回收站 {n} 项',
|
||||
listHasMore: '(已显示 {n} 项,还有更多)',
|
||||
createdWithName: '已创建:{name}',
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
fetchFailedNotFound: '端点不存在,该厂商可能不支持模型列表拉取:{msg}',
|
||||
toastModelEditLocalOnly: '已启用 / 权重为本地编辑,下次拉取时覆盖',
|
||||
|
||||
// 模型特征标签(模态 + 能力 + 探测来源;cost/intel 去除见 UX-260618-04)
|
||||
// 模型特征标签(模态 + 能力 + 探测来源;cost/intel 去除见 )
|
||||
tagModality: { text: '文本', vision: '视觉', audio: '音频', video: '视频' },
|
||||
tagCapability: { tool_use: '工具调用', embedding: '向量', code_gen: '代码' },
|
||||
tagSource: { user_set: '手动', preset_table: '预设', heuristic: '推断', default: '默认' },
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ watch(
|
||||
)
|
||||
|
||||
/**
|
||||
* 估算单条消息 parts 的字符体积(base64/url 字符串长度之和,UX-260617-20)。
|
||||
* 估算单条消息 parts 的字符体积(base64/url 字符串长度之和,)。
|
||||
*
|
||||
* 只计 image 片的 base64 与 url(大体量载荷);text 片(正文/工具结果文本)不在此预算口径内,
|
||||
* 避免对常规长文本消息误伤。无 parts 或非 image 片返回 0。
|
||||
|
||||
@@ -52,7 +52,7 @@ export function parseTags(tags: string | null | undefined): string[] {
|
||||
}
|
||||
|
||||
// ── 知识 label 文案辅助(列表 Knowledge.vue 与详情 KnowledgeDetail.vue 共用) ──
|
||||
// 原两文件各内联一份重复实现(SW-260618-20 详情迁移时复制),提取至此单一来源。
|
||||
// 原两文件各内联一份重复实现(详情迁移时复制),提取至此单一来源。
|
||||
// 全部依赖 i18n,故用模块级 t(同文件 parseTags 之上的全局 t)。
|
||||
|
||||
/** 知识类型文案(无 icon):${icon} ${text} 组合的文本部分,亦供下拉选项 */
|
||||
|
||||
@@ -29,7 +29,7 @@ function createStore() {
|
||||
// 取代工具执行后需手动刷新。
|
||||
//
|
||||
// task 分支不再无参 loadTasks(会把 store.tasks 冲成全量,破坏
|
||||
// B-260615-29「store.tasks 反映当前筛选」契约)。改为读 _activeTaskProject:
|
||||
// 「store.tasks 反映当前筛选」契约)。改为读 _activeTaskProject:
|
||||
// - undefined: Tasks 视图未挂载/从未设过筛选 → 不触发 task load(避免破坏筛选,
|
||||
// 让 B-29 watch/handle 主导;此时改动由用户下次进 Tasks 自然刷新)
|
||||
// - 'all' : 全量拉取
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* ═══ Markdown 渲染(全局) ═══ */
|
||||
/* CR-260615-09: 从 AiChat.vue / ProjectDetail.vue / Ideas.vue / Knowledge.vue / TaskDetail.vue
|
||||
/* 从 AiChat.vue / ProjectDetail.vue / Ideas.vue / Knowledge.vue / TaskDetail.vue
|
||||
的重复 .ai-md 样式块抽取为全局 CSS,消除 ~350 行重复。
|
||||
各文件独有覆盖保留在原文件 scoped style 中。 */
|
||||
|
||||
@@ -56,11 +56,11 @@
|
||||
border-top: 0.5px solid var(--df-border);
|
||||
margin: 8px 0;
|
||||
}
|
||||
/* B-260618-06:移除 display:block(破坏 table 布局上下文致 tr/td 塌堆叠丢列对齐,用户截图
|
||||
/* 移除 display:block(破坏 table 布局上下文致 tr/td 塌堆叠丢列对齐,用户截图
|
||||
反馈测试报告表格错乱根因)+ 移除 overflow-x:auto(display:table 下浏览器忽略无效)。
|
||||
table 恢复默认 display:table 列对齐恢复;width:100%→max-width:100% 允许窄表格按内容
|
||||
自适应不强制拉满。宽表格横向滚动交由各页面容器承载(AiChat 气泡 .ai-msg-bubble--ai.ai-md
|
||||
加 overflow-x:auto,见 AiChat.vue)。原 UX-260617-21 display:block 方案治标破坏本体。 */
|
||||
加 overflow-x:auto,见 AiChat.vue)。display:block 方案治标破坏本体。 */
|
||||
.ai-md table {
|
||||
max-width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- global.css 已有的 .btn / .modal 系列 / .page-header / .empty-hint / .empty-state
|
||||
不在此重复,仅补充尚未全局化的类
|
||||
|
||||
落地批:DRY 收口(B-260619)
|
||||
落地批:DRY 收口
|
||||
- .info-item / .info-item.info-block / .info-item .label / .info-item .value
|
||||
← ProjectDetail.vue / TaskDetail.vue / TaskOutputCard.vue 三处复制
|
||||
- .panel / .panel-header ← ProjectDetail.vue / TaskDetail.vue 两处复制
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
import { t, currentLocale } from '@/i18n/i18n-helpers'
|
||||
|
||||
// CR-260615-08相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
|
||||
// 相对时间走 i18n(原硬编码中文,en locale 时间恒中文)。
|
||||
// 类型安全访问经 i18n-helpers 收口(规避 vue-i18n TS2589)。
|
||||
|
||||
/** 解析任意时间值为毫秒时间戳;无效或空返回 null(仅本模块内部使用) */
|
||||
|
||||
+1
-1
@@ -483,7 +483,7 @@ async function onUpdateTags(tags: string): Promise<boolean> {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCurrentView()
|
||||
// 从路由参数恢复选中灵感(修复 /ideas/:id 直接访问空白页 B-260615-36)
|
||||
// 从路由参数恢复选中灵感(修复 /ideas/:id 直接访问空白页 )
|
||||
if (route.params.id) {
|
||||
const id = route.params.id as string
|
||||
const exists = store.ideas.some(i => i.id === id)
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 右侧:详情(KnowledgeDetail 子组件,SW-260618-20 拆分)===== -->
|
||||
<!-- ===== 右侧:详情(KnowledgeDetail 子组件,拆分)===== -->
|
||||
<div class="kn-detail-panel">
|
||||
<KnowledgeDetail
|
||||
v-if="detail"
|
||||
@@ -417,7 +417,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.knowledge { padding: 16px 20px 20px; height: 100%; display: flex; flex-direction: column; }
|
||||
.knowledge { padding: 16px 20px 20px; height: 100%; display: flex; flex-direction: column; container-type: inline-size; }
|
||||
|
||||
/* ===== 顶部 Toast 提示(⑤ 操作反馈,对齐 Settings.vue)===== */
|
||||
.toast {
|
||||
@@ -452,7 +452,7 @@ onMounted(() => {
|
||||
|
||||
/* ===== 左右分栏 ===== */
|
||||
.kn-layout {
|
||||
display: grid; grid-template-columns: minmax(240px, 360px) 1fr; gap: var(--df-gap-page);
|
||||
display: grid; grid-template-columns: minmax(240px, 360px) minmax(280px, 1fr); gap: var(--df-gap-page);
|
||||
flex: 1; min-height: 0;
|
||||
}
|
||||
.kn-list-panel, .kn-detail-panel {
|
||||
@@ -526,7 +526,13 @@ onMounted(() => {
|
||||
.form-textarea { min-height: 80px; resize: vertical; font-family: inherit; }
|
||||
|
||||
/* ===== 响应式:窄屏单列布局(详情窄屏样式随 KnowledgeDetail 子组件迁移) ===== */
|
||||
/* 双闸:@media 管窗口本身窄;@container 管「视口不窄但主区被 AI 面板挤压」场景
|
||||
(AI 面板平分内容时 .knowledge 实际宽度 < 620px,media query 感知不到)。 */
|
||||
@media (max-width: 760px) {
|
||||
.kn-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
@container (max-width: 620px) {
|
||||
.kn-layout { grid-template-columns: 1fr; }
|
||||
.kn-detail-panel { overflow-y: visible; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
</div>
|
||||
<div class="info-item info-block">
|
||||
<span class="label">{{ $t('projectDetail.description') }}</span>
|
||||
<!-- B-260615-25:项目描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail)
|
||||
<!-- 项目描述 Markdown 渲染,复用 useMarkdown composable(同 B-24 TaskDetail)
|
||||
P1-g+ 问题10③:长描述折叠(对齐 TaskDetail 长内容),默认折叠截 4 行,展开看全文 -->
|
||||
<div v-if="currentProject.description" class="description-wrap" :class="{ 'is-collapsed': !descExpanded }">
|
||||
<span
|
||||
@@ -493,7 +493,7 @@ const { t, locale } = useI18n()
|
||||
const logListRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// A1[PD-P1-4]:任务列表随 AI 改动刷新 —— 本地 df-data-changed 监听(entity=task)重拉 loadProjectTasks。
|
||||
// 对齐 TaskDetail B-260616-18 模式:onMounted 注册、onBeforeUnmount 释放;连发事件防抖合并,
|
||||
// 对齐 TaskDetail 模式:onMounted 注册、onBeforeUnmount 释放;连发事件防抖合并,
|
||||
// 且仅概览 tab 活跃(任务列表面板在概览)才刷新。
|
||||
let _unlistenDataChanged: UnlistenFn | null = null
|
||||
let _taskReloadTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -1115,7 +1115,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
/* 字段同行布局(label 固定宽 + 值占余),描述字段 info-block 保持块状)
|
||||
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
|
||||
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口),
|
||||
此处仅保留本组件特有覆盖。 */
|
||||
.info-item a.idea-link {
|
||||
color: var(--df-accent);
|
||||
@@ -1314,7 +1314,7 @@ onUnmounted(() => {
|
||||
/* .info-tags .tech-tag 与全局 .tech-tag(components.css)逐字一致,移除局部冗余,
|
||||
标签样式由全局提供。 */
|
||||
|
||||
/* ===== 项目描述 Markdown 渲染(B-260615-25,基础样式收敛至全局 ai-md.css) ===== */
|
||||
/* ===== 项目描述 Markdown 渲染(,基础样式收敛至全局 ai-md.css) ===== */
|
||||
.description.ai-md { font-size: 14px; color: var(--df-text); line-height: 1.6; }
|
||||
/* P1-g+ 问题10③:描述折叠(对齐 TaskDetail 长内容)— 折叠时 max-height 截断 + 渐变遮罩 */
|
||||
.description-wrap { position: relative; }
|
||||
@@ -1475,7 +1475,7 @@ onUnmounted(() => {
|
||||
.wf-progress-hint-fail { color: var(--df-danger); }
|
||||
|
||||
/* ===== 面板 ===== */
|
||||
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619),
|
||||
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 ),
|
||||
此处仅保留本组件特有 .task-count。 */
|
||||
.task-count { font-size: 12px; color: var(--df-text-dim); }
|
||||
.task-filter-chips {
|
||||
|
||||
@@ -533,7 +533,7 @@ async function runImport() {
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* ===== 技术栈标签:基础样式已收敛至全局 components.css(DRY 收口 B-260619),
|
||||
/* ===== 技术栈标签:基础样式已收敛至全局 components.css(DRY 收口 ),
|
||||
创建/导入模态与本页 ProjectCard 渲染的标签均走全局 .tech-tag/.card-tags。 ===== */
|
||||
|
||||
/* ===== 目录选择行 ===== */
|
||||
|
||||
@@ -895,7 +895,7 @@ onBeforeUnmount(() => {
|
||||
min-width: 0; /* 网格项防溢出(长 url/代码块) */
|
||||
}
|
||||
|
||||
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 B-260619)。 */
|
||||
/* .panel / .panel-header 基础样式已收敛至全局 components.css(DRY 收口 )。 */
|
||||
|
||||
/* ===== 信息列表 ===== */
|
||||
.task-info {
|
||||
@@ -904,7 +904,7 @@ onBeforeUnmount(() => {
|
||||
gap: var(--df-gap-grid);
|
||||
}
|
||||
/* 字段同行布局(label 固定宽 + value 占余),描述字段 info-block 保持块状)
|
||||
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口 B-260619),
|
||||
基础 .info-item/.label/.value 已收敛至全局 components.css(DRY 收口),
|
||||
此处仅保留本组件特有覆盖。 */
|
||||
/* B-24 漏 white-space 覆盖致 v-html 后 HTML 标签间 \n 被 pre-wrap 渲染为空行间距——移除 pre-wrap,描述由 .ai-md 接管(line-height 1.5 对齐 AiChat li)。
|
||||
Problem4 重设计:描述区从 .info-item 内移到独立 .description-wrap(可折叠容器),
|
||||
|
||||
Reference in New Issue
Block a user