From c6454531d81c70bdcabf0e96fb93676d24221912 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 13:11:59 +0800 Subject: [PATCH 01/19] fix(dashboard): prioritize home orb live overview signals --- .../dashboard/dashboard.contract.test.ts | 92 +++++++++++ .../home/components/DashboardEventPanel.tsx | 18 ++- .../dashboard/home/dashboardHome.service.ts | 147 +++++++++++++++++- .../dashboard/home/dashboardHome.types.ts | 14 ++ 4 files changed, 266 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 4732fa62f..e9d8ce18f 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1770,6 +1770,15 @@ test("dashboard home no longer replays mock summon or voice presets when live re assert.match(dashboardHomeSource, /data\.loadWarnings\.length > 0/); }); +test("dashboard event panel routes task-detail actions through the shared navigation helper", () => { + const panelSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/home/components/DashboardEventPanel.tsx"), "utf8"); + + assert.match(panelSource, /navigateToDashboardTaskDetail/); + assert.match(panelSource, /target\?\.kind === "task_detail"/); + assert.match(panelSource, /resolvePrimaryActionLabel/); + assert.match(panelSource, /activeState\.navigationTarget\?\.label/); +}); + test("mirror page stays RPC-only instead of exposing a page-level mock toggle", () => { const mirrorAppSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/memory/MirrorApp.tsx"), "utf8"); @@ -6525,6 +6534,89 @@ test("dashboard home keeps module and recommendation failures local instead of b ); }); +test("dashboard home prioritizes live overview summons and task-detail targets over recommendation-only fallback copy", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record; + summonTemplates: Array<{ message: string; nextStep?: string; reason: string; stateKey: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.summonTemplates.length, 1); + assert.equal(data.summonTemplates[0]?.message, "整理 Q3 复盘要点"); + assert.equal(data.summonTemplates[0]?.nextStep, "打开任务详情"); + assert.match(data.summonTemplates[0]?.reason ?? "", /刚生成了新的摘要草稿/); + assert.equal(data.summonTemplates[0]?.stateKey, "task_working"); + assert.equal(data.stateMap.task_working?.navigationTarget?.kind, "task_detail"); + assert.equal(data.stateMap.task_working?.navigationTarget?.taskId, "task_focus_001"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + + if (moduleName === "tasks") { + return { + highlights: ["继续推进当前摘要任务"], + module: moduleName, + summary: { + blocked_tasks: 0, + focus_runtime_summary: { + active_steering_count: 0, + events_count: 1, + latest_event_type: null, + loop_stop_reason: null, + observation_signals: [], + }, + focus_task_id: "task_focus_001", + processing_tasks: 1, + waiting_auth_tasks: 0, + }, + tab: "focus", + }; + } + + return { + highlights: [], + module: moduleName, + summary: {}, + tab: "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: { + current_step: "生成摘要", + next_action: "等待处理完成", + status: "processing", + task_id: "task_focus_001", + title: "整理 Q3 复盘要点", + updated_at: "2026-04-07T10:40:00+08:00", + }, + high_value_signal: ["刚生成了新的摘要草稿。"], + quick_actions: ["打开任务详情"], + trust_summary: { + has_restore_point: true, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + }, + ); +}); + test("security service no longer imports governance mocks into product runtime", () => { const securityServiceSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/safety/securityService.ts"), "utf8"); diff --git a/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx b/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx index bbc4cabe9..9352eabd6 100644 --- a/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx +++ b/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx @@ -3,6 +3,7 @@ import { Archive, ArrowRight, BadgeAlert, BarChart3, BellOff, BrainCircuit, Cale import { AnimatePresence, motion } from "motion/react"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; +import { navigateToDashboardTaskDetail } from "@/features/dashboard/shared/dashboardTaskDetailNavigation"; import { resolveDashboardModuleRoutePath } from "@/features/dashboard/shared/dashboardRouteTargets"; import type { DashboardHomeContextItem, DashboardHomeEventStateKey, DashboardHomeModuleKey, DashboardHomeSignalItem, DashboardHomeStateData, DashboardHomeStateGroup } from "../dashboardHome.types"; @@ -96,13 +97,26 @@ function modulePrimaryLabel(module: DashboardHomeModuleKey) { return labels[module]; } +function resolvePrimaryActionLabel(activeState: DashboardHomeStateData) { + return activeState.navigationTarget?.label ?? modulePrimaryLabel(activeState.module); +} + export function DashboardEventPanel({ activeState, onClose, onStateChange, stateGroups, stateMap }: DashboardEventPanelProps) { const navigate = useNavigate(); function handleOpenModule(module: DashboardHomeModuleKey) { onClose(); window.setTimeout(() => { - navigate(resolveDashboardModuleRoutePath(module)); + const target = activeState?.navigationTarget; + + // Home states can promote a task-detail deep link without changing the + // formal dashboard overview contract, so keep task routing local here. + if (target?.kind === "task_detail") { + navigateToDashboardTaskDetail(navigate, target.taskId); + return; + } + + navigate(resolveDashboardModuleRoutePath(target?.module ?? module)); }, 0); } @@ -260,7 +274,7 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state
diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 1a8ec4460..661b133ae 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -21,6 +21,7 @@ import type { DashboardHomeContextItem, DashboardHomeEventStateKey, DashboardHomeInsightItem, + DashboardHomeNavigationTarget, DashboardHomeModuleKey, DashboardHomeNoteItem, DashboardHomeSignalItem, @@ -44,6 +45,13 @@ const dashboardModuleTabs: Record = { tasks: "focus", }; +const dashboardModuleActionLabels: Record = { + memory: "打开镜子页", + notes: "打开便签页", + safety: "打开安全页", + tasks: "打开任务页", +}; + const dashboardModuleNextSteps: Record = { memory: "打开镜子页查看本周总结", notes: "打开便签页继续整理事项", @@ -108,6 +116,7 @@ function cloneStateData(state: DashboardHomeStateData): DashboardHomeStateData { anomaly: state.anomaly ? { ...state.anomaly } : undefined, context: state.context.map((item) => ({ ...item })), insights: state.insights?.map((item) => ({ ...item })), + navigationTarget: state.navigationTarget ? { ...state.navigationTarget } : undefined, notes: state.notes?.map((item) => ({ ...item })), progressSteps: state.progressSteps?.map((item) => ({ ...item })), signals: state.signals?.map((item) => ({ ...item })), @@ -199,6 +208,70 @@ function getSignalLevel(riskLevel: RiskLevel): DashboardHomeSignalItem["level"] return "normal"; } +function buildModuleNavigationTarget( + module: DashboardHomeModuleKey, + label = dashboardModuleActionLabels[module], +): DashboardHomeNavigationTarget { + return { + kind: "module", + label, + module, + }; +} + +function buildTaskDetailNavigationTarget(taskId: string, label = "打开任务详情"): DashboardHomeNavigationTarget { + return { + kind: "task_detail", + label, + module: "tasks", + taskId, + }; +} + +function buildTaskNavigationTarget( + focusSummary: NonNullable, +): DashboardHomeNavigationTarget { + if (focusSummary.status === "waiting_auth") { + return buildModuleNavigationTarget("safety", "前往授权"); + } + + if (focusSummary.status === "completed") { + return buildTaskDetailNavigationTarget(focusSummary.task_id, "查看交付结果"); + } + + return buildTaskDetailNavigationTarget(focusSummary.task_id); +} + +function getOverviewSignals(overview: AgentDashboardOverviewGetResult) { + return Array.isArray(overview.overview.high_value_signal) + ? overview.overview.high_value_signal.filter((signal): signal is string => typeof signal === "string" && signal.trim() !== "") + : []; +} + +function getOverviewQuickActions(overview: AgentDashboardOverviewGetResult) { + return Array.isArray(overview.overview.quick_actions) + ? overview.overview.quick_actions.filter((action): action is string => typeof action === "string" && action.trim() !== "") + : []; +} + +function getSummonNextStep(quickActions: string[], state: DashboardHomeStateData) { + return quickActions[0] ?? state.navigationTarget?.label ?? dashboardModuleNextSteps[state.module]; +} + +function dedupeSummonTemplates(templates: Array>) { + const seen = new Set(); + + return templates.filter((template) => { + const key = `${template.stateKey}::${template.message}::${template.reason}`; + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); +} + function getModuleHighlights(result: AgentDashboardModuleGetResult | null | undefined) { return Array.isArray(result?.highlights) ? result.highlights.filter(Boolean) : []; } @@ -417,6 +490,7 @@ function buildTaskState( const state = cloneStateData(dashboardHomeStates[stateKey]); const focusSummary = overview.overview.focus_summary; const runtimeSummary = getTaskModuleRuntimeSummary(taskModule, focusSummary?.task_id).focusRuntimeSummary; + state.navigationTarget = buildModuleNavigationTarget("tasks"); if (!focusSummary) { const highlights = getModuleHighlights(taskModule); @@ -440,6 +514,7 @@ function buildTaskState( state.tag = formatTaskTag(focusSummary.status); state.progressLabel = runtimeSummary.active_steering_count > 0 ? `待消费要求 ${runtimeSummary.active_steering_count} 条` : focusSummary.next_action; state.context = buildTaskContext(overview, taskModule); + state.navigationTarget = buildTaskNavigationTarget(focusSummary); if (focusSummary.status === "confirming_intent") { state.anomaly = { @@ -519,6 +594,7 @@ function buildNotesState( }, ]; state.notes = noteItems.length > 0 ? noteItems : state.notes; + state.navigationTarget = buildModuleNavigationTarget("notes"); return state; } @@ -550,6 +626,7 @@ function buildMemoryState(stateKey: DashboardHomeEventStateKey, memoryModule: Ag text: item, type: index === 0 ? "active" : "hint", })); + state.navigationTarget = buildModuleNavigationTarget("memory"); return state; } @@ -559,6 +636,10 @@ function buildSafetyState(stateKey: DashboardHomeEventStateKey, overview: AgentD const trustSummary = overview.overview.trust_summary; const highlights = getModuleHighlights(safetyModule); const riskLabel = formatRiskLabel(trustSummary.risk_level); + state.navigationTarget = buildModuleNavigationTarget( + "safety", + trustSummary.pending_authorizations > 0 ? "处理待授权操作" : "查看安全详情", + ); state.headline = trustSummary.pending_authorizations > 0 @@ -658,6 +739,62 @@ function getSummonPriority(module: DashboardHomeModuleKey, stateKey: DashboardHo return "normal"; } +function buildOverviewSummons( + overview: AgentDashboardOverviewGetResult, + stateKeys: Record, + stateMap: Record, +): Array> { + const quickActions = getOverviewQuickActions(overview); + const highValueSignals = getOverviewSignals(overview); + const focusSummary = overview.overview.focus_summary; + const trustSummary = overview.overview.trust_summary; + const safetyState = stateMap[stateKeys.safety]; + const taskState = stateMap[stateKeys.tasks]; + const templates: Array> = []; + + // Keep the first summon anchored to formal overview fields so the home orb + // surfaces live task/security signals before softer recommendation copy. + if (trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green") { + templates.push({ + duration: 6_200, + message: highValueSignals[0] ?? safetyState.headline, + nextStep: getSummonNextStep(quickActions, safetyState), + priority: "urgent", + reason: safetyState.subline, + stateKey: stateKeys.safety, + }); + } + + if (focusSummary) { + const overflowSignal = highValueSignals.find((signal) => signal !== templates[0]?.message); + templates.push({ + duration: 6_000, + message: focusSummary.title, + nextStep: getSummonNextStep(quickActions, taskState), + priority: getSummonPriority("tasks", stateKeys.tasks), + reason: [focusSummary.current_step, focusSummary.next_action, overflowSignal].filter(Boolean).join(" · "), + stateKey: stateKeys.tasks, + }); + } + + if (templates.length === 0 && highValueSignals[0]) { + const targetState = trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green" + ? safetyState + : taskState; + + templates.push({ + duration: 5_800, + message: highValueSignals[0], + nextStep: getSummonNextStep(quickActions, targetState), + priority: targetState.module === "safety" ? "urgent" : getSummonPriority(targetState.module, targetState.key), + reason: targetState.subline, + stateKey: targetState.key, + }); + } + + return dedupeSummonTemplates(templates); +} + function buildRecommendationSummons( recommendations: RecommendationItem[], stateKeys: Record, @@ -678,7 +815,7 @@ function buildRecommendationSummons( } satisfies Omit; }); - return templates; + return dedupeSummonTemplates(templates); } function buildVoiceSequences( @@ -711,12 +848,14 @@ function buildFocusLine( ) { if (overview.overview.focus_summary) { const runtimeSummary = getTaskModuleRuntimeSummary(taskModule, overview.overview.focus_summary.task_id).focusRuntimeSummary; + const overviewSignals = getOverviewSignals(overview); + return { headline: overview.overview.focus_summary.title, reason: [ overview.overview.focus_summary.current_step, overview.overview.focus_summary.next_action, - runtimeSummary.latest_event_type ?? runtimeSummary.loop_stop_reason, + runtimeSummary.latest_event_type ?? runtimeSummary.loop_stop_reason ?? overviewSignals[0], ] .filter(Boolean) .join(" · "), @@ -750,7 +889,9 @@ function buildDashboardHomeData(input: { stateMap[stateKeys.memory] = buildMemoryState(stateKeys.memory, input.moduleResults.memory); stateMap[stateKeys.safety] = buildSafetyState(stateKeys.safety, input.overview, input.moduleResults.safety); - const summonTemplates = buildRecommendationSummons(input.recommendations.items, stateKeys, input.moduleResults); + const overviewSummons = buildOverviewSummons(input.overview, stateKeys, stateMap); + const recommendationSummons = buildRecommendationSummons(input.recommendations.items, stateKeys, input.moduleResults); + const summonTemplates = dedupeSummonTemplates([...overviewSummons, ...recommendationSummons]); return { focusLine: buildFocusLine(input.overview, input.moduleResults.tasks, summonTemplates), diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts index 887c056d3..1ab425c71 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts @@ -60,6 +60,19 @@ export type DashboardHomeAnomaly = { severity: "warn" | "error" | "info"; }; +export type DashboardHomeNavigationTarget = + | { + kind: "module"; + label: string; + module: DashboardHomeModuleKey; + } + | { + kind: "task_detail"; + label: string; + module: "tasks"; + taskId: string; + }; + export type DashboardHomeStateData = { key: DashboardHomeEventStateKey; module: DashboardHomeModuleKey; @@ -79,6 +92,7 @@ export type DashboardHomeStateData = { insights?: DashboardHomeInsightItem[]; signals?: DashboardHomeSignalItem[]; anomaly?: DashboardHomeAnomaly; + navigationTarget?: DashboardHomeNavigationTarget; breathSpeed: number; }; From 43335667c9bfec7fa600b74bab22a98bf0b9c2c5 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 13:41:50 +0800 Subject: [PATCH 02/19] fix(dashboard): balance home orb summon signals --- .../src/app/dashboard/DashboardHome.tsx | 46 +++++- .../dashboard/dashboard.contract.test.ts | 132 ++++++++++++++++++ .../dashboard/home/dashboardHome.presets.ts | 4 + .../dashboard/home/dashboardHome.service.ts | 33 ++++- .../dashboard/home/dashboardHome.types.ts | 1 + 5 files changed, 211 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/app/dashboard/DashboardHome.tsx b/apps/desktop/src/app/dashboard/DashboardHome.tsx index 0f8d4707e..607206e77 100644 --- a/apps/desktop/src/app/dashboard/DashboardHome.tsx +++ b/apps/desktop/src/app/dashboard/DashboardHome.tsx @@ -49,6 +49,34 @@ function getCenterState(activeStateKey: DashboardHomeEventStateKey | null) { return "hover_input" as const; } +function pickNextSummonIndex( + templates: Array>, + previousIndex: number, + previousModule: DashboardHomeModuleKey | null, +) { + const total = templates.length; + if (total <= 1) { + return 0; + } + + const candidateIndexes = templates + .map((template, index) => ({ index, module: template.module })) + .filter((candidate) => candidate.index !== previousIndex && candidate.module !== previousModule) + .map((candidate) => candidate.index); + + const fallbackIndexes = templates + .map((_, index) => index) + .filter((index) => index !== previousIndex); + + const pool = candidateIndexes.length > 0 ? candidateIndexes : fallbackIndexes; + const nextIndex = pool[Math.floor(Math.random() * pool.length)]; + if (nextIndex !== previousIndex) { + return nextIndex; + } + + return (nextIndex + 1) % total; +} + type DashboardHomeProps = { data: DashboardHomeData; onVoiceOpen: () => void; @@ -69,8 +97,9 @@ export function DashboardHome({ const [hoveredEntranceKey, setHoveredEntranceKey] = useState(null); const [activeStateKey, setActiveStateKey] = useState(null); const [summons, setSummons] = useState([]); - const summonIndexRef = useRef(0); const summonIdRef = useRef(0); + const lastSummonIndexRef = useRef(-1); + const lastSummonModuleRef = useRef(null); const summonTimerRef = useRef(null); const activeState = activeStateKey ? data.stateMap[activeStateKey] : null; @@ -87,8 +116,16 @@ export function DashboardHome({ return; } - const template = data.summonTemplates[summonIndexRef.current % data.summonTemplates.length]; - summonIndexRef.current += 1; + // Summons are local presentation state, so balancing the module order here + // does not change the formal dashboard data contract or backend ranking. + const nextIndex = pickNextSummonIndex( + data.summonTemplates, + lastSummonIndexRef.current, + lastSummonModuleRef.current, + ); + lastSummonIndexRef.current = nextIndex; + const template = data.summonTemplates[nextIndex]; + lastSummonModuleRef.current = template.module; setSummons((current) => { if (current.length >= 1) { @@ -109,8 +146,9 @@ export function DashboardHome({ }, [data.summonTemplates]); useEffect(() => { - summonIndexRef.current = 0; summonIdRef.current = 0; + lastSummonIndexRef.current = -1; + lastSummonModuleRef.current = null; setSummons([]); if (data.summonTemplates.length === 0) { diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index e9d8ce18f..43ea6727c 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1779,6 +1779,16 @@ test("dashboard event panel routes task-detail actions through the shared naviga assert.match(panelSource, /activeState\.navigationTarget\?\.label/); }); +test("dashboard home randomizes summons while preferring a different module when alternatives exist", () => { + const dashboardHomeSource = readFileSync(resolve(desktopRoot, "src/app/dashboard/DashboardHome.tsx"), "utf8"); + + assert.match(dashboardHomeSource, /function pickNextSummonIndex\(/); + assert.match(dashboardHomeSource, /candidate\.module !== previousModule/); + assert.match(dashboardHomeSource, /const pool = candidateIndexes\.length > 0 \? candidateIndexes : fallbackIndexes/); + assert.match(dashboardHomeSource, /Math\.floor\(Math\.random\(\) \* pool\.length\)/); + assert.match(dashboardHomeSource, /lastSummonModuleRef\.current = template\.module/); +}); + test("mirror page stays RPC-only instead of exposing a page-level mock toggle", () => { const mirrorAppSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/memory/MirrorApp.tsx"), "utf8"); @@ -6617,6 +6627,128 @@ test("dashboard home prioritizes live overview summons and task-detail targets o ); }); +test("dashboard home prioritizes overview and module signals before recommendation-only fallback copy", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record; + summonTemplates: Array<{ message: string; module: string; nextStep?: string; reason: string; stateKey: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.ok(data.summonTemplates.length >= 4); + assert.deepEqual( + data.summonTemplates.slice(0, 4).map((item) => item.module), + ["safety", "tasks", "notes", "memory"], + ); + assert.equal(data.summonTemplates[0]?.message, "刚生成了新的摘要草稿。"); + assert.equal(data.summonTemplates[1]?.nextStep, "打开任务详情"); + assert.equal(data.stateMap.task_working?.navigationTarget?.kind, "task_detail"); + assert.equal(data.stateMap.task_working?.navigationTarget?.taskId, "task_focus_001"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + + if (moduleName === "tasks") { + return { + highlights: ["继续推进当前摘要任务"], + module: moduleName, + summary: { + blocked_tasks: 0, + focus_runtime_summary: { + active_steering_count: 0, + events_count: 1, + latest_event_type: null, + loop_stop_reason: null, + observation_signals: [], + }, + focus_task_id: "task_focus_001", + processing_tasks: 1, + waiting_auth_tasks: 1, + }, + tab: "focus", + }; + } + + if (moduleName === "notes") { + return { + highlights: ["两条便签接近执行窗口", "建议先整理今日提醒"], + module: moduleName, + summary: { + completed_tasks: 3, + exceptions: 1, + }, + tab: "queue", + }; + } + + if (moduleName === "memory") { + return { + highlights: ["本周复盘已经形成初稿", "最近三次协作都提到了同一风险边界"], + module: moduleName, + summary: {}, + tab: "overview", + }; + } + + if (moduleName === "safety") { + return { + highlights: ["建议先处理待授权操作,再继续推进其它任务。"], + module: moduleName, + summary: {}, + tab: "guard", + }; + } + + return { + highlights: [], + module: moduleName, + summary: {}, + tab: "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: { + current_step: "生成摘要", + next_action: "等待处理完成", + status: "processing", + task_id: "task_focus_001", + title: "整理 Q3 复盘要点", + updated_at: "2026-04-07T10:40:00+08:00", + }, + high_value_signal: ["刚生成了新的摘要草稿。"], + quick_actions: ["打开任务详情"], + trust_summary: { + has_restore_point: true, + pending_authorizations: 2, + risk_level: "yellow", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [ + { + feedback_score: 0.8, + intent: { confidence: 0.8, name: "task_follow_up" }, + recommendation_id: "rec_001", + text: "继续推进当前任务。", + }, + ], + }), + }, + ); +}); + test("security service no longer imports governance mocks into product runtime", () => { const securityServiceSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/safety/securityService.ts"), "utf8"); diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.presets.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.presets.ts index 699da35f3..b2bc1eb60 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.presets.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.presets.ts @@ -406,6 +406,7 @@ export const dashboardSummonTemplates: Omit[] = { duration: 6400, message: "你有一个任务正在快速接近收尾", + module: "tasks", nextStep: "点开任务页继续推进", priority: "urgent", reason: "当前主任务已经进入草稿确认阶段", @@ -414,6 +415,7 @@ export const dashboardSummonTemplates: Omit[] = { duration: 5600, message: "昨晚留下的便签里,有 2 条已经变成可执行事项", + module: "notes", nextStep: "去便签页看我整理出来的顺序", priority: "normal", reason: "已识别 2 项可直接推进,1 项需确认优先级", @@ -422,6 +424,7 @@ export const dashboardSummonTemplates: Omit[] = { duration: 5400, message: "镜子里浮出了一条新的节奏洞察", + module: "memory", nextStep: "打开镜子页查看这周的模式总结", priority: "normal", reason: "我记住了你最近新的深度工作时段", @@ -430,6 +433,7 @@ export const dashboardSummonTemplates: Omit[] = { duration: 5200, message: "有一条边界提醒值得你先看一眼", + module: "safety", nextStep: "去安全页确认风险与恢复点摘要", priority: "low", reason: "当前有一项操作需要你确认是否继续", diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 661b133ae..fafd38c14 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -758,6 +758,7 @@ function buildOverviewSummons( templates.push({ duration: 6_200, message: highValueSignals[0] ?? safetyState.headline, + module: "safety", nextStep: getSummonNextStep(quickActions, safetyState), priority: "urgent", reason: safetyState.subline, @@ -770,6 +771,7 @@ function buildOverviewSummons( templates.push({ duration: 6_000, message: focusSummary.title, + module: "tasks", nextStep: getSummonNextStep(quickActions, taskState), priority: getSummonPriority("tasks", stateKeys.tasks), reason: [focusSummary.current_step, focusSummary.next_action, overflowSignal].filter(Boolean).join(" · "), @@ -785,6 +787,7 @@ function buildOverviewSummons( templates.push({ duration: 5_800, message: highValueSignals[0], + module: targetState.module, nextStep: getSummonNextStep(quickActions, targetState), priority: targetState.module === "safety" ? "urgent" : getSummonPriority(targetState.module, targetState.key), reason: targetState.subline, @@ -795,6 +798,32 @@ function buildOverviewSummons( return dedupeSummonTemplates(templates); } +function buildModuleSummarySummons( + stateKeys: Record, + stateMap: Record, +): Array> { + const modules: DashboardHomeModuleKey[] = ["notes", "memory", "safety", "tasks"]; + + return dedupeSummonTemplates( + modules.flatMap((module) => { + const state = stateMap[stateKeys[module]]; + if (!state.headline.trim()) { + return []; + } + + return [{ + duration: 5_600, + message: state.headline, + module, + nextStep: state.navigationTarget?.label ?? dashboardModuleNextSteps[module], + priority: getSummonPriority(module, state.key), + reason: state.subline, + stateKey: state.key, + } satisfies Omit]; + }), + ); +} + function buildRecommendationSummons( recommendations: RecommendationItem[], stateKeys: Record, @@ -807,6 +836,7 @@ function buildRecommendationSummons( return { duration: 6_200, message: item.text, + module, nextStep: dashboardModuleNextSteps[module], priority: getSummonPriority(module, stateKeys[module]), reason: highlights[0] ?? `来自 ${dashboardModuleLabels[module]} 模块的实时建议`, @@ -890,8 +920,9 @@ function buildDashboardHomeData(input: { stateMap[stateKeys.safety] = buildSafetyState(stateKeys.safety, input.overview, input.moduleResults.safety); const overviewSummons = buildOverviewSummons(input.overview, stateKeys, stateMap); + const moduleSummons = buildModuleSummarySummons(stateKeys, stateMap); const recommendationSummons = buildRecommendationSummons(input.recommendations.items, stateKeys, input.moduleResults); - const summonTemplates = dedupeSummonTemplates([...overviewSummons, ...recommendationSummons]); + const summonTemplates = dedupeSummonTemplates([...overviewSummons, ...moduleSummons, ...recommendationSummons]); return { focusLine: buildFocusLine(input.overview, input.moduleResults.tasks, summonTemplates), diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts index 1ab425c71..c1969a7f7 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts @@ -124,6 +124,7 @@ export type DashboardHomeSummonEvent = { copyDuration?: number; id: string; hideCopy?: boolean; + module: DashboardHomeModuleKey; stateKey: DashboardHomeEventStateKey; message: string; reason: string; From 00742cb2e5500b0aed4c3d7d983ad5152ca96d17 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 15:00:21 +0800 Subject: [PATCH 03/19] fix(dashboard): align home mirror copy with overview --- .../dashboard/dashboard.contract.test.ts | 85 +++++++++ .../dashboard/home/dashboardHome.service.ts | 180 +++++++++++++++++- 2 files changed, 258 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 43ea6727c..c2abe5d6e 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1047,6 +1047,7 @@ type DashboardContractRpcMethodOverrides = { convertNotepadToTask?: (params: AgentNotepadConvertToTaskParams) => Promise; getDashboardModule?: (params: unknown) => Promise; getDashboardOverview?: (params: unknown) => Promise; + getMirrorOverview?: (params: unknown) => Promise; getRecommendations?: (params: unknown) => Promise; getMirrorOverviewDetailed?: (params: unknown) => Promise; getSecuritySummary?: (params: unknown) => Promise; @@ -1190,6 +1191,9 @@ function withDesktopAliasRuntime( getDashboardOverview: rpcMethods?.getDashboardOverview ?? (() => Promise.reject(new Error("getDashboardOverview should not run in dashboard contract tests"))), + getMirrorOverview: + rpcMethods?.getMirrorOverview ?? + (() => Promise.reject(new Error("getMirrorOverview should not run in dashboard contract tests"))), getRecommendations: rpcMethods?.getRecommendations ?? (() => Promise.reject(new Error("getRecommendations should not run in dashboard contract tests"))), @@ -6749,6 +6753,87 @@ test("dashboard home prioritizes overview and module signals before recommendati ); }); +test("dashboard home reuses formal mirror profile fields for memory copy", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.stateMap.memory_summary?.headline, "用户画像"); + assert.equal(data.stateMap.memory_summary?.subline, "工作风格:偏好即时结果回显"); + assert.equal(data.stateMap.memory_summary?.context?.[0]?.text, "后端画像字段 3 项"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + + if (moduleName === "notes") { + return { + highlights: ["最近恢复点 rp_1777961976151255500 已可用于安全回显。", "最近审计动作:generate_text -> openai_responses:deepseek-v4-flas..."], + module: moduleName, + summary: { + completed_tasks: 2, + exceptions: 1, + }, + tab: "queue", + }; + } + + if (moduleName === "memory") { + return { + highlights: ["最近恢复点 rp_1777961976151255500 已可用于安全回显。", "最近审计动作:generate_text -> openai_responses:deepseek-v4-flas..."], + module: moduleName, + summary: {}, + tab: "overview", + }; + } + + return { + highlights: [], + module: moduleName, + summary: {}, + tab: moduleName === "tasks" ? "focus" : "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: true, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: ["这里是历史概要,不该覆盖用户画像文案。"], + memory_references: [], + profile: { + active_hours: "16-21h", + preferred_output: "bubble", + work_style: "偏好即时结果回显", + }, + }), + }, + ); +}); + test("security service no longer imports governance mocks into product runtime", () => { const securityServiceSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/safety/securityService.ts"), "utf8"); diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index fafd38c14..7cfcab9e9 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -1,6 +1,7 @@ import type { AgentDashboardModuleGetResult, AgentDashboardOverviewGetResult, + AgentMirrorOverviewGetResult, AgentRecommendationGetResult, RecommendationFeedback, RecommendationItem, @@ -11,6 +12,7 @@ import type { import { getDashboardModule, getDashboardOverview, + getMirrorOverview, getRecommendations, submitRecommendationFeedback, } from "@/rpc/methods"; @@ -276,6 +278,14 @@ function getModuleHighlights(result: AgentDashboardModuleGetResult | null | unde return Array.isArray(result?.highlights) ? result.highlights.filter(Boolean) : []; } +function isCrossDomainGovernanceHighlight(text: string) { + return /恢复点|审计|授权|回显|风险|generate_text|openai_responses|tool_call|workspace/i.test(text); +} + +function getMemoryHighlights(result: AgentDashboardModuleGetResult | null | undefined) { + return getModuleHighlights(result).filter((item) => !isCrossDomainGovernanceHighlight(item)); +} + function getModuleSummaryNumber(result: AgentDashboardModuleGetResult | null | undefined, key: string) { const value = result?.summary?.[key]; return typeof value === "number" ? value : 0; @@ -601,7 +611,7 @@ function buildNotesState( function buildMemoryInsights(memoryModule: AgentDashboardModuleGetResult): DashboardHomeInsightItem[] { const icons = ["brain", "time", "repeat", "chat"] as const; - const highlights = getModuleHighlights(memoryModule); + const highlights = getMemoryHighlights(memoryModule); if (highlights.length === 0) { return cloneStateData(dashboardHomeStates.memory_summary).insights ?? []; @@ -614,12 +624,159 @@ function buildMemoryInsights(memoryModule: AgentDashboardModuleGetResult): Dashb })); } -function buildMemoryState(stateKey: DashboardHomeEventStateKey, memoryModule: AgentDashboardModuleGetResult) { +function buildFormalMirrorInsights(overview: AgentMirrorOverviewGetResult): DashboardHomeInsightItem[] { + const items: DashboardHomeInsightItem[] = []; + + if (overview.profile) { + items.push( + { + emphasis: true, + iconKey: "brain", + text: `工作风格:${overview.profile.work_style}`, + }, + { + iconKey: "chat", + text: `偏好交付:${overview.profile.preferred_output}`, + }, + { + iconKey: "time", + text: `活跃时段:${overview.profile.active_hours}`, + }, + ); + } + + if (items.length > 0) { + return items; + } + + const latestReference = overview.memory_references[0] ?? null; + if (latestReference) { + return [ + { + emphasis: true, + iconKey: "brain", + text: latestReference.memory_id, + }, + { + iconKey: "time", + text: latestReference.summary || latestReference.reason, + }, + ]; + } + + if (overview.history_summary[0]) { + return [ + { + emphasis: true, + iconKey: "repeat", + text: overview.history_summary[0], + }, + ]; + } + + return []; +} + +function buildMemoryHeadline(highlights: string[]) { + if (highlights.length >= 2) { + return "本周镜子观察"; + } + + return "最近协作镜像"; +} + +function buildMemorySubline(highlights: string[]) { + return highlights[0] ?? "镜子会持续整理近期协作节奏和重复出现的模式。"; +} + +function buildFormalMirrorState(overview: AgentMirrorOverviewGetResult) { + const profile = overview.profile; + if (profile) { + return { + context: [ + { + iconKey: "brain", + text: "后端画像字段 3 项", + type: "active" as const, + }, + { + iconKey: "chat", + text: `偏好交付:${profile.preferred_output}`, + type: "normal" as const, + }, + { + iconKey: "time", + text: `活跃时段:${profile.active_hours}`, + type: "hint" as const, + }, + ], + headline: "用户画像", + insights: buildFormalMirrorInsights(overview), + subline: `工作风格:${profile.work_style}`, + }; + } + + const latestReference = overview.memory_references[0] ?? null; + if (latestReference) { + return { + context: [ + { + iconKey: "brain", + text: `最近记忆引用:${latestReference.memory_id}`, + type: "active" as const, + }, + { + iconKey: "repeat", + text: latestReference.summary || latestReference.reason, + type: "hint" as const, + }, + ], + headline: "近期被调用记忆", + insights: buildFormalMirrorInsights(overview), + subline: latestReference.summary || latestReference.reason, + }; + } + + if (overview.history_summary[0]) { + return { + context: [ + { + iconKey: "repeat", + text: overview.history_summary[0], + type: "active" as const, + }, + ], + headline: "历史概要", + insights: buildFormalMirrorInsights(overview), + subline: overview.history_summary[1] ?? overview.history_summary[0], + }; + } + + return null; +} + +function buildMemoryState( + stateKey: DashboardHomeEventStateKey, + memoryModule: AgentDashboardModuleGetResult, + mirrorOverview: AgentMirrorOverviewGetResult | null, +) { const state = cloneStateData(dashboardHomeStates[stateKey]); - const highlights = getModuleHighlights(memoryModule); + if (mirrorOverview) { + const formalMirrorState = buildFormalMirrorState(mirrorOverview); + if (formalMirrorState) { + state.headline = formalMirrorState.headline; + state.subline = formalMirrorState.subline; + state.insights = formalMirrorState.insights; + state.context = formalMirrorState.context; + state.navigationTarget = buildModuleNavigationTarget("memory"); + return state; + } + } - state.headline = highlights[0] ?? state.headline; - state.subline = highlights[1] ?? highlights[0] ?? state.subline; + const highlights = getMemoryHighlights(memoryModule); + + state.headline = buildMemoryHeadline(highlights); + state.subline = buildMemorySubline(highlights); state.insights = buildMemoryInsights(memoryModule); state.context = highlights.slice(0, 3).map((item, index) => ({ iconKey: index === 0 ? "brain" : index === 1 ? "repeat" : "time", @@ -908,6 +1065,7 @@ function buildFocusLine( function buildDashboardHomeData(input: { loadWarnings: string[]; moduleResults: Record; + mirrorOverview: AgentMirrorOverviewGetResult | null; overview: AgentDashboardOverviewGetResult; recommendations: AgentRecommendationGetResult; }): DashboardHomeData { @@ -916,7 +1074,7 @@ function buildDashboardHomeData(input: { stateMap[stateKeys.tasks] = buildTaskState(stateKeys.tasks, input.overview, input.moduleResults.tasks); stateMap[stateKeys.notes] = buildNotesState(stateKeys.notes, input.moduleResults.notes, input.recommendations.items); - stateMap[stateKeys.memory] = buildMemoryState(stateKeys.memory, input.moduleResults.memory); + stateMap[stateKeys.memory] = buildMemoryState(stateKeys.memory, input.moduleResults.memory, input.mirrorOverview); stateMap[stateKeys.safety] = buildSafetyState(stateKeys.safety, input.overview, input.moduleResults.safety); const overviewSummons = buildOverviewSummons(input.overview, stateKeys, stateMap); @@ -956,7 +1114,7 @@ function formatDashboardHomeLoadWarning(label: string, error: unknown) { } export async function loadDashboardHomeData(): Promise { - const [overviewResult, tasksResult, notesResult, memoryResult, safetyResult, recommendationsResult] = await Promise.allSettled([ + const [overviewResult, tasksResult, notesResult, memoryResult, safetyResult, recommendationsResult, mirrorOverviewResult] = await Promise.allSettled([ getDashboardOverview({ focus_mode: false, include: ["focus_summary", "trust_summary", "quick_actions", "high_value_signal"], @@ -991,6 +1149,10 @@ export async function loadDashboardHomeData(): Promise { scene: "idle", source: "dashboard", }), + getMirrorOverview({ + include: ["profile", "history_summary", "daily_summary", "memory_references"], + request_meta: createRequestMeta("dashboard_home_mirror_overview"), + }), ]); if (overviewResult.status === "rejected") { @@ -1013,6 +1175,9 @@ export async function loadDashboardHomeData(): Promise { const recommendations = recommendationsResult.status === "fulfilled" ? recommendationsResult.value : (loadWarnings.push(formatDashboardHomeLoadWarning("建议流", recommendationsResult.reason)), createEmptyRecommendationResult()); + const mirrorOverview = mirrorOverviewResult.status === "fulfilled" + ? mirrorOverviewResult.value + : null; return buildDashboardHomeData({ loadWarnings, @@ -1022,6 +1187,7 @@ export async function loadDashboardHomeData(): Promise { safety: safetyModule, tasks: tasksModule, }, + mirrorOverview, overview: overviewResult.value, recommendations, }); From 2cdb9e4c1fd90837362617c5db2782608b353c88 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 15:02:37 +0800 Subject: [PATCH 04/19] fix(dashboard): drop fake empty-note home copy --- .../dashboard/dashboard.contract.test.ts | 78 +++++++++++++++- .../dashboard/home/dashboardHome.service.ts | 91 ++++++++++++++----- 2 files changed, 140 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index c2abe5d6e..a03f7f827 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -6646,11 +6646,9 @@ test("dashboard home prioritizes overview and module signals before recommendati const data = await service.loadDashboardHomeData(); - assert.ok(data.summonTemplates.length >= 4); - assert.deepEqual( - data.summonTemplates.slice(0, 4).map((item) => item.module), - ["safety", "tasks", "notes", "memory"], - ); + assert.ok(data.summonTemplates.length >= 3); + assert.deepEqual(data.summonTemplates.slice(0, 3).map((item) => item.module), ["safety", "tasks", "memory"]); + assert.equal(data.summonTemplates.some((item) => item.module === "notes"), false); assert.equal(data.summonTemplates[0]?.message, "刚生成了新的摘要草稿。"); assert.equal(data.summonTemplates[1]?.nextStep, "打开任务详情"); assert.equal(data.stateMap.task_working?.navigationTarget?.kind, "task_detail"); @@ -6834,6 +6832,76 @@ test("dashboard home reuses formal mirror profile fields for memory copy", async ); }); +test("dashboard home keeps notes copy module-native and skips fake empty-note summons", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record }>; + summonTemplates: Array<{ module: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.stateMap.notes_scheduled?.headline, "这里还没有可协作的事项"); + assert.equal(data.stateMap.notes_scheduled?.subline, "当前例外项 1 条,建议优先整理最接近执行窗口的事项。"); + assert.equal(data.stateMap.notes_scheduled?.context?.[0]?.text, "暂无便签"); + assert.equal(data.summonTemplates.some((item) => item.module === "notes"), false); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + + if (moduleName === "notes") { + return { + highlights: ["最近恢复点 rp_1777961976151255500 已可用于安全回显。", "最近审计动作:generate_text -> openai_responses:deepseek-v4-flas..."], + module: moduleName, + summary: { + completed_tasks: 2, + exceptions: 1, + }, + tab: "queue", + }; + } + + return { + highlights: [], + module: moduleName, + summary: {}, + tab: moduleName === "tasks" ? "focus" : "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: true, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [], + profile: null, + }), + }, + ); +}); + test("security service no longer imports governance mocks into product runtime", () => { const securityServiceSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/safety/securityService.ts"), "utf8"); diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 7cfcab9e9..e5ca29076 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -282,6 +282,10 @@ function isCrossDomainGovernanceHighlight(text: string) { return /恢复点|审计|授权|回显|风险|generate_text|openai_responses|tool_call|workspace/i.test(text); } +function getNotesHighlights(result: AgentDashboardModuleGetResult | null | undefined) { + return getModuleHighlights(result).filter((item) => !isCrossDomainGovernanceHighlight(item)); +} + function getMemoryHighlights(result: AgentDashboardModuleGetResult | null | undefined) { return getModuleHighlights(result).filter((item) => !isCrossDomainGovernanceHighlight(item)); } @@ -570,39 +574,74 @@ function buildNotesItems(recommendations: RecommendationItem[]): DashboardHomeNo })); } +function buildNotesHeadline(noteItems: DashboardHomeNoteItem[]) { + if (noteItems.length > 0) { + return `近期便签 ${noteItems.length} 条待整理`; + } + + return "这里还没有可协作的事项"; +} + +function buildNotesSubline(noteItems: DashboardHomeNoteItem[], exceptions: number) { + if (noteItems[0]) { + return noteItems[0].text; + } + + if (exceptions === 0) { + return "等你把想记住的事情交给便签协作后,这里会按近期要做、后续安排、重复事项和已结束四组方式整理出来。"; + } + + return `当前例外项 ${exceptions} 条,建议优先整理最接近执行窗口的事项。`; +} + +function buildNotesContext(noteItems: DashboardHomeNoteItem[], highlights: string[]) { + if (noteItems.length > 0) { + return [ + { + iconKey: "note", + text: `推荐事项 ${noteItems.length} 条`, + type: "active" as const, + }, + { + iconKey: "calendar", + text: noteItems[0]?.text ?? "打开便签页查看待处理事项。", + type: "hint" as const, + }, + { + iconKey: "repeat", + text: highlights[0] ?? "打开便签页查看正式状态。", + type: "normal" as const, + }, + ]; + } + + return [ + { + iconKey: "note", + text: "暂无便签", + type: "normal" as const, + }, + { + iconKey: "calendar", + text: "等你把想记住的事情交给便签协作后,这里会按近期要做、后续安排、重复事项和已结束四组方式整理出来。", + type: "hint" as const, + }, + ]; +} + function buildNotesState( stateKey: DashboardHomeEventStateKey, notesModule: AgentDashboardModuleGetResult, recommendations: RecommendationItem[], ) { const state = cloneStateData(dashboardHomeStates[stateKey]); - const highlights = getModuleHighlights(notesModule); + const highlights = getNotesHighlights(notesModule); const noteItems = buildNotesItems(recommendations); - const completedTasks = getModuleSummaryNumber(notesModule, "completed_tasks"); const exceptions = getModuleSummaryNumber(notesModule, "exceptions"); - state.headline = noteItems[0]?.text ?? highlights[0] ?? `便签池里有 ${completedTasks} 条已整理记录`; - state.subline = - highlights[0] && highlights[1] - ? `${highlights[0]} ${highlights[1]}` - : highlights[0] ?? `当前例外项 ${exceptions} 条,建议优先整理最接近执行窗口的事项。`; - state.context = [ - { - iconKey: "note", - text: `推荐事项 ${noteItems.length || 0} 条`, - type: noteItems.length > 0 ? "active" : "normal", - }, - { - iconKey: "repeat", - text: `已完成任务 ${completedTasks} 条`, - type: "normal", - }, - { - iconKey: "calendar", - text: highlights[0] ?? "保持便签整理节奏,准备转正式任务。", - type: "hint", - }, - ]; + state.headline = buildNotesHeadline(noteItems); + state.subline = buildNotesSubline(noteItems, exceptions); + state.context = buildNotesContext(noteItems, highlights); state.notes = noteItems.length > 0 ? noteItems : state.notes; state.navigationTarget = buildModuleNavigationTarget("notes"); @@ -968,6 +1007,10 @@ function buildModuleSummarySummons( return []; } + if (module === "notes" && state.headline === "这里还没有可协作的事项") { + return []; + } + return [{ duration: 5_600, message: state.headline, From e1f89b04ba1b97f43916f65feabcd36c611658e3 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 15:26:23 +0800 Subject: [PATCH 05/19] fix(dashboard): align home notes copy with notepad buckets --- .../dashboard/dashboard.contract.test.ts | 77 ++++++++- .../dashboard/home/dashboardHome.service.ts | 151 ++++++++++++++++-- .../dashboard/notes/notePage.service.ts | 2 +- 3 files changed, 219 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index a03f7f827..0c89b7ba0 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -6544,6 +6544,15 @@ test("dashboard home keeps module and recommendation failures local instead of b getRecommendations: async () => { throw new Error("recommendations unavailable"); }, + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), }, ); }); @@ -6627,6 +6636,15 @@ test("dashboard home prioritizes live overview summons and task-detail targets o cooldown_hit: false, items: [], }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), }, ); }); @@ -6648,7 +6666,6 @@ test("dashboard home prioritizes overview and module signals before recommendati assert.ok(data.summonTemplates.length >= 3); assert.deepEqual(data.summonTemplates.slice(0, 3).map((item) => item.module), ["safety", "tasks", "memory"]); - assert.equal(data.summonTemplates.some((item) => item.module === "notes"), false); assert.equal(data.summonTemplates[0]?.message, "刚生成了新的摘要草稿。"); assert.equal(data.summonTemplates[1]?.nextStep, "打开任务详情"); assert.equal(data.stateMap.task_working?.navigationTarget?.kind, "task_detail"); @@ -6747,6 +6764,46 @@ test("dashboard home prioritizes overview and module signals before recommendati }, ], }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: ["本周复盘已经形成初稿", "最近三次协作都提到了同一风险边界"], + memory_references: [], + profile: null, + }), + listNotepad: async (params: unknown) => { + const group = (params as { group?: string }).group; + if (group === "upcoming") { + return { + items: [ + { + agent_suggestion: "先处理这个事项。", + bucket: "upcoming", + due_at: "2026-04-07T18:00:00+08:00", + item_id: "todo_home_001", + status: "due_today", + title: "重要客户邮件回复", + type: "note", + }, + ], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 1, + }, + }; + } + + return { + items: [], + page: { + has_more: false, + limit: group === "closed" ? 24 : 12, + offset: 0, + total: 0, + }, + }; + }, }, ); }); @@ -6828,6 +6885,15 @@ test("dashboard home reuses formal mirror profile fields for memory copy", async work_style: "偏好即时结果回显", }, }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), }, ); }); @@ -6898,6 +6964,15 @@ test("dashboard home keeps notes copy module-native and skips fake empty-note su memory_references: [], profile: null, }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), }, ); }); diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index e5ca29076..ae6342e50 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -16,6 +16,9 @@ import { getRecommendations, submitRecommendationFeedback, } from "@/rpc/methods"; +import { describeNotePreview, buildNoteSummary } from "@/features/dashboard/notes/notePage.mapper"; +import { loadNoteBucket } from "@/features/dashboard/notes/notePage.service"; +import type { NoteListItem } from "@/features/dashboard/notes/notePage.types"; import { dashboardHomeStates, } from "./dashboardHome.presets"; @@ -95,6 +98,16 @@ type DashboardTaskRuntimeSummary = { }; }; +type DashboardHomeNoteBuckets = { + closed: NoteListItem[]; + later: NoteListItem[]; + primaryItem: NoteListItem | null; + recurring: NoteListItem[]; + summary: ReturnType; + upcoming: NoteListItem[]; + warnings: string[]; +}; + const emptyFocusRuntimeSummary: DashboardTaskRuntimeSummary["focusRuntimeSummary"] = { active_steering_count: 0, events_count: 0, @@ -278,6 +291,39 @@ function getModuleHighlights(result: AgentDashboardModuleGetResult | null | unde return Array.isArray(result?.highlights) ? result.highlights.filter(Boolean) : []; } +async function loadDashboardHomeNoteBuckets(): Promise { + const [upcomingResult, laterResult, recurringResult, closedResult] = await Promise.allSettled([ + loadNoteBucket("upcoming"), + loadNoteBucket("later"), + loadNoteBucket("recurring_rule"), + loadNoteBucket("closed"), + ]); + const warnings: string[] = []; + const upcoming = upcomingResult.status === "fulfilled" + ? upcomingResult.value.items + : (warnings.push(formatDashboardHomeLoadWarning("便签近期要做", upcomingResult.reason)), []); + const later = laterResult.status === "fulfilled" + ? laterResult.value.items + : (warnings.push(formatDashboardHomeLoadWarning("便签后续安排", laterResult.reason)), []); + const recurring = recurringResult.status === "fulfilled" + ? recurringResult.value.items + : (warnings.push(formatDashboardHomeLoadWarning("便签重复事项", recurringResult.reason)), []); + const closed = closedResult.status === "fulfilled" + ? closedResult.value.items + : (warnings.push(formatDashboardHomeLoadWarning("便签已结束", closedResult.reason)), []); + const primaryItem = upcoming[0] ?? later[0] ?? recurring[0] ?? closed[0] ?? null; + + return { + closed, + later, + primaryItem, + recurring, + summary: buildNoteSummary({ recurring_rule: recurring, upcoming }), + upcoming, + warnings, + }; +} + function isCrossDomainGovernanceHighlight(text: string) { return /恢复点|审计|授权|回显|风险|generate_text|openai_responses|tool_call|workspace/i.test(text); } @@ -417,6 +463,26 @@ function getNotesStateKey(notesModule: AgentDashboardModuleGetResult, recommenda return "notes_scheduled"; } +function getNotesStateKeyFromBuckets(noteBuckets: DashboardHomeNoteBuckets | null) { + if (!noteBuckets) { + return null; + } + + if (noteBuckets.primaryItem?.item.bucket === "recurring_rule") { + return "notes_reminder" as const; + } + + if (noteBuckets.primaryItem) { + return "notes_processing" as const; + } + + if (noteBuckets.recurring.length > 0) { + return "notes_reminder" as const; + } + + return "notes_scheduled" as const; +} + function getMemoryStateKey(memoryModule: AgentDashboardModuleGetResult) { return getModuleHighlights(memoryModule).length > 1 ? "memory_summary" : "memory_habit"; } @@ -574,7 +640,32 @@ function buildNotesItems(recommendations: RecommendationItem[]): DashboardHomeNo })); } -function buildNotesHeadline(noteItems: DashboardHomeNoteItem[]) { +function mapHomeNotesFromBuckets(noteBuckets: DashboardHomeNoteBuckets) { + return [ + ...noteBuckets.upcoming, + ...noteBuckets.later, + ...noteBuckets.recurring, + ...noteBuckets.closed, + ].slice(0, 3).map((item) => ({ + id: item.item.item_id, + status: item.item.bucket === "closed" + ? "done" + : item.item.bucket === "recurring_rule" + ? "recurring" + : item.item.status === "normal" + ? "pending" + : "processing", + tag: item.experience.summaryLabel, + text: item.item.title, + time: item.experience.timeHint, + } satisfies DashboardHomeNoteItem)); +} + +function buildNotesHeadline(noteItems: DashboardHomeNoteItem[], noteBuckets: DashboardHomeNoteBuckets | null) { + if (noteBuckets?.primaryItem) { + return noteBuckets.primaryItem.item.title; + } + if (noteItems.length > 0) { return `近期便签 ${noteItems.length} 条待整理`; } @@ -594,7 +685,27 @@ function buildNotesSubline(noteItems: DashboardHomeNoteItem[], exceptions: numbe return `当前例外项 ${exceptions} 条,建议优先整理最接近执行窗口的事项。`; } -function buildNotesContext(noteItems: DashboardHomeNoteItem[], highlights: string[]) { +function buildNotesContext(noteItems: DashboardHomeNoteItem[], highlights: string[], noteBuckets: DashboardHomeNoteBuckets | null) { + if (noteBuckets?.primaryItem) { + return [ + { + iconKey: "note", + text: `${noteBuckets.primaryItem.experience.summaryLabel} · ${describeNotePreview(noteBuckets.primaryItem.item, noteBuckets.primaryItem.experience)}`, + type: "active" as const, + }, + { + iconKey: "calendar", + text: `今日待处理 ${noteBuckets.summary.dueToday} 条 · 已逾期 ${noteBuckets.summary.overdue} 条`, + type: "normal" as const, + }, + { + iconKey: "repeat", + text: `今日重复 ${noteBuckets.summary.recurringToday} 条 · 适合转任务 ${noteBuckets.summary.readyForAgent} 条`, + type: "hint" as const, + }, + ]; + } + if (noteItems.length > 0) { return [ { @@ -633,15 +744,16 @@ function buildNotesState( stateKey: DashboardHomeEventStateKey, notesModule: AgentDashboardModuleGetResult, recommendations: RecommendationItem[], + noteBuckets: DashboardHomeNoteBuckets | null, ) { const state = cloneStateData(dashboardHomeStates[stateKey]); const highlights = getNotesHighlights(notesModule); - const noteItems = buildNotesItems(recommendations); + const noteItems = noteBuckets ? mapHomeNotesFromBuckets(noteBuckets) : buildNotesItems(recommendations); const exceptions = getModuleSummaryNumber(notesModule, "exceptions"); - state.headline = buildNotesHeadline(noteItems); + state.headline = buildNotesHeadline(noteItems, noteBuckets); state.subline = buildNotesSubline(noteItems, exceptions); - state.context = buildNotesContext(noteItems, highlights); + state.context = buildNotesContext(noteItems, highlights, noteBuckets); state.notes = noteItems.length > 0 ? noteItems : state.notes; state.navigationTarget = buildModuleNavigationTarget("notes"); @@ -998,7 +1110,7 @@ function buildModuleSummarySummons( stateKeys: Record, stateMap: Record, ): Array> { - const modules: DashboardHomeModuleKey[] = ["notes", "memory", "safety", "tasks"]; + const modules: DashboardHomeModuleKey[] = ["notes", "memory"]; return dedupeSummonTemplates( modules.flatMap((module) => { @@ -1011,6 +1123,10 @@ function buildModuleSummarySummons( return []; } + if (module === "memory" && state.headline === "最近协作镜像" && state.subline === "镜子会持续整理近期协作节奏和重复出现的模式。") { + return []; + } + return [{ duration: 5_600, message: state.headline, @@ -1109,14 +1225,20 @@ function buildDashboardHomeData(input: { loadWarnings: string[]; moduleResults: Record; mirrorOverview: AgentMirrorOverviewGetResult | null; + noteBuckets: DashboardHomeNoteBuckets | null; overview: AgentDashboardOverviewGetResult; recommendations: AgentRecommendationGetResult; }): DashboardHomeData { const stateMap = createBaseStateMap(); - const stateKeys = getModuleStateKeyMap(input.overview, input.moduleResults, input.recommendations.items); + const noteBucketsStateKey = getNotesStateKeyFromBuckets(input.noteBuckets); + const inferredStateKeys = getModuleStateKeyMap(input.overview, input.moduleResults, input.recommendations.items); + const stateKeys = { + ...inferredStateKeys, + notes: noteBucketsStateKey ?? inferredStateKeys.notes, + }; stateMap[stateKeys.tasks] = buildTaskState(stateKeys.tasks, input.overview, input.moduleResults.tasks); - stateMap[stateKeys.notes] = buildNotesState(stateKeys.notes, input.moduleResults.notes, input.recommendations.items); + stateMap[stateKeys.notes] = buildNotesState(stateKeys.notes, input.moduleResults.notes, input.recommendations.items, input.noteBuckets); stateMap[stateKeys.memory] = buildMemoryState(stateKeys.memory, input.moduleResults.memory, input.mirrorOverview); stateMap[stateKeys.safety] = buildSafetyState(stateKeys.safety, input.overview, input.moduleResults.safety); @@ -1157,7 +1279,7 @@ function formatDashboardHomeLoadWarning(label: string, error: unknown) { } export async function loadDashboardHomeData(): Promise { - const [overviewResult, tasksResult, notesResult, memoryResult, safetyResult, recommendationsResult, mirrorOverviewResult] = await Promise.allSettled([ + const [overviewResult, tasksResult, notesResult, memoryResult, safetyResult, recommendationsResult, mirrorOverviewResult, noteBucketsResult] = await Promise.allSettled([ getDashboardOverview({ focus_mode: false, include: ["focus_summary", "trust_summary", "quick_actions", "high_value_signal"], @@ -1196,6 +1318,7 @@ export async function loadDashboardHomeData(): Promise { include: ["profile", "history_summary", "daily_summary", "memory_references"], request_meta: createRequestMeta("dashboard_home_mirror_overview"), }), + loadDashboardHomeNoteBuckets(), ]); if (overviewResult.status === "rejected") { @@ -1221,6 +1344,15 @@ export async function loadDashboardHomeData(): Promise { const mirrorOverview = mirrorOverviewResult.status === "fulfilled" ? mirrorOverviewResult.value : null; + const noteBuckets = noteBucketsResult.status === "fulfilled" + ? noteBucketsResult.value + : null; + + if (noteBuckets) { + loadWarnings.push(...noteBuckets.warnings); + } else if (noteBucketsResult.status === "rejected") { + loadWarnings.push(formatDashboardHomeLoadWarning("便签详情", noteBucketsResult.reason)); + } return buildDashboardHomeData({ loadWarnings, @@ -1231,6 +1363,7 @@ export async function loadDashboardHomeData(): Promise { tasks: tasksModule, }, mirrorOverview, + noteBuckets, overview: overviewResult.value, recommendations, }); diff --git a/apps/desktop/src/features/dashboard/notes/notePage.service.ts b/apps/desktop/src/features/dashboard/notes/notePage.service.ts index 60c15aac6..31c4ad848 100644 --- a/apps/desktop/src/features/dashboard/notes/notePage.service.ts +++ b/apps/desktop/src/features/dashboard/notes/notePage.service.ts @@ -681,7 +681,7 @@ async function withTimeout(promise: Promise, label: string): Promise { return Promise.race([ promise, new Promise((_, reject) => { - window.setTimeout(() => reject(new Error(`${label} 请求超时`)), NOTEPAD_RPC_TIMEOUT_MS); + globalThis.setTimeout(() => reject(new Error(`${label} 请求超时`)), NOTEPAD_RPC_TIMEOUT_MS); }), ]); } From f06f2d6990c41ad4ea3f88ec2d7ad6a9a9edf568 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 20:02:34 +0800 Subject: [PATCH 06/19] fix(dashboard): align remaining home orb module copy --- .../dashboard/dashboard.contract.test.ts | 138 +++++++++++++++++- .../dashboard/home/dashboardHome.service.ts | 54 ++++--- 2 files changed, 169 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 0c89b7ba0..e2c8a9558 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -6512,8 +6512,8 @@ test("dashboard home keeps module and recommendation failures local instead of b assert.equal(data.loadWarnings.length, 2); assert.match(data.loadWarnings[0], /便签摘要同步失败:notes module unavailable/); assert.match(data.loadWarnings[1], /建议流同步失败:recommendations unavailable/); - assert.equal(data.focusLine.headline, "首页总览已经连接到真实任务轨道。"); - assert.equal(data.summonTemplates.length, 0); + assert.equal(data.focusLine.headline, "当前整体风险等级为 低"); + assert.equal(data.summonTemplates.length, 1); assert.equal(data.voiceSequences.length, 0); }, { @@ -6572,7 +6572,7 @@ test("dashboard home prioritizes live overview summons and task-detail targets o const data = await service.loadDashboardHomeData(); - assert.equal(data.summonTemplates.length, 1); + assert.ok(data.summonTemplates.length >= 1); assert.equal(data.summonTemplates[0]?.message, "整理 Q3 复盘要点"); assert.equal(data.summonTemplates[0]?.nextStep, "打开任务详情"); assert.match(data.summonTemplates[0]?.reason ?? "", /刚生成了新的摘要草稿/); @@ -6898,6 +6898,138 @@ test("dashboard home reuses formal mirror profile fields for memory copy", async ); }); +test("dashboard home keeps a low-priority safety summon available when the formal trust chain is green but recoverable", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + summonTemplates: Array<{ message: string; module: string; priority: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.summonTemplates[0]?.module, "safety"); + assert.equal(data.summonTemplates[0]?.message, "最近恢复点可用"); + assert.equal(data.summonTemplates[0]?.priority, "low"); + }, + { + getDashboardModule: async (params: unknown) => ({ + highlights: [], + module: (params as { module?: string }).module ?? "unknown", + summary: {}, + tab: "overview", + }), + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: true, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [], + profile: null, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + +test("dashboard home prefers formal mirror references over profile copy when both exist", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.stateMap.memory_habit?.headline, "近期被调用记忆"); + assert.equal(data.stateMap.memory_habit?.subline, "本周战略复盘已被近期任务再次引用。"); + assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "最近记忆引用:memory_strategy_weekly"); + }, + { + getDashboardModule: async (params: unknown) => ({ + highlights: [], + module: (params as { module?: string }).module ?? "unknown", + summary: {}, + tab: "overview", + }), + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: false, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: ["这里有历史概要,但不该覆盖近期记忆引用。"], + memory_references: [ + { + memory_id: "memory_strategy_weekly", + reason: "近期任务再次命中这段长期记忆。", + summary: "本周战略复盘已被近期任务再次引用。", + }, + ], + profile: { + active_hours: "16-21h", + preferred_output: "bubble", + work_style: "偏好即时结果回显", + }, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home keeps notes copy module-native and skips fake empty-note summons", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index ae6342e50..4f68bc7a7 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -841,50 +841,50 @@ function buildMemorySubline(highlights: string[]) { } function buildFormalMirrorState(overview: AgentMirrorOverviewGetResult) { - const profile = overview.profile; - if (profile) { + const latestReference = overview.memory_references[0] ?? null; + if (latestReference) { return { context: [ { iconKey: "brain", - text: "后端画像字段 3 项", + text: `最近记忆引用:${latestReference.memory_id}`, type: "active" as const, }, { - iconKey: "chat", - text: `偏好交付:${profile.preferred_output}`, - type: "normal" as const, - }, - { - iconKey: "time", - text: `活跃时段:${profile.active_hours}`, + iconKey: "repeat", + text: latestReference.summary || latestReference.reason, type: "hint" as const, }, ], - headline: "用户画像", + headline: "近期被调用记忆", insights: buildFormalMirrorInsights(overview), - subline: `工作风格:${profile.work_style}`, + subline: latestReference.summary || latestReference.reason, }; } - const latestReference = overview.memory_references[0] ?? null; - if (latestReference) { + const profile = overview.profile; + if (profile) { return { context: [ { iconKey: "brain", - text: `最近记忆引用:${latestReference.memory_id}`, + text: "后端画像字段 3 项", type: "active" as const, }, { - iconKey: "repeat", - text: latestReference.summary || latestReference.reason, + iconKey: "chat", + text: `偏好交付:${profile.preferred_output}`, + type: "normal" as const, + }, + { + iconKey: "time", + text: `活跃时段:${profile.active_hours}`, type: "hint" as const, }, ], - headline: "近期被调用记忆", + headline: "用户画像", insights: buildFormalMirrorInsights(overview), - subline: latestReference.summary || latestReference.reason, + subline: `工作风格:${profile.work_style}`, }; } @@ -1062,7 +1062,9 @@ function buildOverviewSummons( // Keep the first summon anchored to formal overview fields so the home orb // surfaces live task/security signals before softer recommendation copy. - if (trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green") { + const hasUrgentSafetySignal = trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green"; + + if (hasUrgentSafetySignal) { templates.push({ duration: 6_200, message: highValueSignals[0] ?? safetyState.headline, @@ -1087,6 +1089,18 @@ function buildOverviewSummons( }); } + if (!hasUrgentSafetySignal && (trustSummary.has_restore_point || safetyState.subline.trim() !== "")) { + templates.push({ + duration: 5_600, + message: trustSummary.has_restore_point ? "最近恢复点可用" : safetyState.headline, + module: "safety", + nextStep: getSummonNextStep(quickActions, safetyState), + priority: "low", + reason: safetyState.subline, + stateKey: stateKeys.safety, + }); + } + if (templates.length === 0 && highValueSignals[0]) { const targetState = trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green" ? safetyState From 8710ee0185a185e192592a79c2b958e808362869 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 20:31:06 +0800 Subject: [PATCH 07/19] fix(dashboard): warn on home mirror overview gaps --- .../src/features/dashboard/dashboard.contract.test.ts | 6 +++++- .../src/features/dashboard/home/dashboardHome.service.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index e2c8a9558..2295a188a 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -6509,9 +6509,10 @@ test("dashboard home keeps module and recommendation failures local instead of b const data = await service.loadDashboardHomeData(); assert.equal(data.stateGroups.length, 4); - assert.equal(data.loadWarnings.length, 2); + assert.equal(data.loadWarnings.length, 3); assert.match(data.loadWarnings[0], /便签摘要同步失败:notes module unavailable/); assert.match(data.loadWarnings[1], /建议流同步失败:recommendations unavailable/); + assert.match(data.loadWarnings[2], /镜子概览同步失败:mirror overview unavailable/); assert.equal(data.focusLine.headline, "当前整体风险等级为 低"); assert.equal(data.summonTemplates.length, 1); assert.equal(data.voiceSequences.length, 0); @@ -6544,6 +6545,9 @@ test("dashboard home keeps module and recommendation failures local instead of b getRecommendations: async () => { throw new Error("recommendations unavailable"); }, + getMirrorOverview: async () => { + throw new Error("mirror overview unavailable"); + }, listNotepad: async () => ({ items: [], page: { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 4f68bc7a7..3c8275986 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -1357,7 +1357,7 @@ export async function loadDashboardHomeData(): Promise { : (loadWarnings.push(formatDashboardHomeLoadWarning("建议流", recommendationsResult.reason)), createEmptyRecommendationResult()); const mirrorOverview = mirrorOverviewResult.status === "fulfilled" ? mirrorOverviewResult.value - : null; + : (loadWarnings.push(formatDashboardHomeLoadWarning("镜子概览", mirrorOverviewResult.reason)), null); const noteBuckets = noteBucketsResult.status === "fulfilled" ? noteBucketsResult.value : null; From 6d466ff10cf5df8ff1e0e93ab1350f68578d0a3d Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 21:26:45 +0800 Subject: [PATCH 08/19] fix(dashboard): ignore closed-only note orbs --- .../dashboard/dashboard.contract.test.ts | 86 +++++++++++++++++++ .../dashboard/home/dashboardHome.service.ts | 2 +- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 2295a188a..02e013141 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -7113,6 +7113,92 @@ test("dashboard home keeps notes copy module-native and skips fake empty-note su ); }); +test("dashboard home does not promote closed-only notes into the active note summon path", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record; + summonTemplates: Array<{ module: string; message: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.stateMap.notes_scheduled?.headline, "这里还没有可协作的事项"); + assert.equal(data.summonTemplates.some((item) => item.module === "notes"), false); + }, + { + getDashboardModule: async (params: unknown) => ({ + highlights: [], + module: (params as { module?: string }).module ?? "unknown", + summary: {}, + tab: "overview", + }), + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: false, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [], + profile: null, + }), + listNotepad: async (params: unknown) => { + const group = (params as { group?: string }).group; + if (group === "closed") { + return { + items: [ + { + agent_suggestion: null, + bucket: "closed", + due_at: null, + item_id: "todo_closed_001", + status: "completed", + title: "历史已结束事项", + type: "archive", + }, + ], + page: { + has_more: false, + limit: 24, + offset: 0, + total: 1, + }, + }; + } + + return { + items: [], + page: { + has_more: false, + limit: group === "closed" ? 24 : 12, + offset: 0, + total: 0, + }, + }; + }, + }, + ); +}); + test("security service no longer imports governance mocks into product runtime", () => { const securityServiceSource = readFileSync(resolve(desktopRoot, "src/features/dashboard/safety/securityService.ts"), "utf8"); diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 3c8275986..3b7f0b4bc 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -311,7 +311,7 @@ async function loadDashboardHomeNoteBuckets(): Promise const closed = closedResult.status === "fulfilled" ? closedResult.value.items : (warnings.push(formatDashboardHomeLoadWarning("便签已结束", closedResult.reason)), []); - const primaryItem = upcoming[0] ?? later[0] ?? recurring[0] ?? closed[0] ?? null; + const primaryItem = upcoming[0] ?? later[0] ?? recurring[0] ?? null; return { closed, From 50921d2d08a973c8241138ab879746fa361ba05b Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Wed, 6 May 2026 21:34:28 +0800 Subject: [PATCH 09/19] fix(dashboard): align safety and mirror home orbs --- .../dashboard/dashboard.contract.test.ts | 5 +- .../dashboard/home/dashboardHome.service.ts | 52 +++++++++---------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 02e013141..696d18eff 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -6910,7 +6910,7 @@ test("dashboard home keeps a low-priority safety summon available when the forma const service = requireFn(modulePath) as { loadDashboardHomeData: () => Promise<{ - summonTemplates: Array<{ message: string; module: string; priority: string }>; + summonTemplates: Array<{ message: string; module: string; nextStep?: string; priority: string }>; }>; }; @@ -6918,6 +6918,7 @@ test("dashboard home keeps a low-priority safety summon available when the forma assert.equal(data.summonTemplates[0]?.module, "safety"); assert.equal(data.summonTemplates[0]?.message, "最近恢复点可用"); + assert.equal(data.summonTemplates[0]?.nextStep, "查看安全详情"); assert.equal(data.summonTemplates[0]?.priority, "low"); }, { @@ -6931,7 +6932,7 @@ test("dashboard home keeps a low-priority safety summon available when the forma overview: { focus_summary: null, high_value_signal: [], - quick_actions: [], + quick_actions: ["打开任务详情"], trust_summary: { has_restore_point: true, pending_authorizations: 0, diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 3b7f0b4bc..9a309601e 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -269,8 +269,12 @@ function getOverviewQuickActions(overview: AgentDashboardOverviewGetResult) { : []; } -function getSummonNextStep(quickActions: string[], state: DashboardHomeStateData) { - return quickActions[0] ?? state.navigationTarget?.label ?? dashboardModuleNextSteps[state.module]; +function getSummonNextStep(state: DashboardHomeStateData, quickActions?: string[]) { + if (state.module === "tasks" && quickActions?.[0]) { + return quickActions[0]; + } + + return state.navigationTarget?.label ?? dashboardModuleNextSteps[state.module]; } function dedupeSummonTemplates(templates: Array>) { @@ -776,41 +780,35 @@ function buildMemoryInsights(memoryModule: AgentDashboardModuleGetResult): Dashb } function buildFormalMirrorInsights(overview: AgentMirrorOverviewGetResult): DashboardHomeInsightItem[] { - const items: DashboardHomeInsightItem[] = []; - - if (overview.profile) { - items.push( + const latestReference = overview.memory_references[0] ?? null; + if (latestReference) { + return [ { emphasis: true, iconKey: "brain", - text: `工作风格:${overview.profile.work_style}`, - }, - { - iconKey: "chat", - text: `偏好交付:${overview.profile.preferred_output}`, + text: latestReference.memory_id, }, { iconKey: "time", - text: `活跃时段:${overview.profile.active_hours}`, + text: latestReference.summary || latestReference.reason, }, - ); - } - - if (items.length > 0) { - return items; + ]; } - const latestReference = overview.memory_references[0] ?? null; - if (latestReference) { + if (overview.profile) { return [ { emphasis: true, iconKey: "brain", - text: latestReference.memory_id, + text: `工作风格:${overview.profile.work_style}`, + }, + { + iconKey: "chat", + text: `偏好交付:${overview.profile.preferred_output}`, }, { iconKey: "time", - text: latestReference.summary || latestReference.reason, + text: `活跃时段:${overview.profile.active_hours}`, }, ]; } @@ -1069,7 +1067,7 @@ function buildOverviewSummons( duration: 6_200, message: highValueSignals[0] ?? safetyState.headline, module: "safety", - nextStep: getSummonNextStep(quickActions, safetyState), + nextStep: getSummonNextStep(safetyState), priority: "urgent", reason: safetyState.subline, stateKey: stateKeys.safety, @@ -1082,19 +1080,19 @@ function buildOverviewSummons( duration: 6_000, message: focusSummary.title, module: "tasks", - nextStep: getSummonNextStep(quickActions, taskState), + nextStep: getSummonNextStep(taskState, quickActions), priority: getSummonPriority("tasks", stateKeys.tasks), reason: [focusSummary.current_step, focusSummary.next_action, overflowSignal].filter(Boolean).join(" · "), stateKey: stateKeys.tasks, }); } - if (!hasUrgentSafetySignal && (trustSummary.has_restore_point || safetyState.subline.trim() !== "")) { + if (!hasUrgentSafetySignal && trustSummary.has_restore_point) { templates.push({ duration: 5_600, - message: trustSummary.has_restore_point ? "最近恢复点可用" : safetyState.headline, + message: "最近恢复点可用", module: "safety", - nextStep: getSummonNextStep(quickActions, safetyState), + nextStep: getSummonNextStep(safetyState), priority: "low", reason: safetyState.subline, stateKey: stateKeys.safety, @@ -1110,7 +1108,7 @@ function buildOverviewSummons( duration: 5_800, message: highValueSignals[0], module: targetState.module, - nextStep: getSummonNextStep(quickActions, targetState), + nextStep: getSummonNextStep(targetState, quickActions), priority: targetState.module === "safety" ? "urgent" : getSummonPriority(targetState.module, targetState.key), reason: targetState.subline, stateKey: targetState.key, From 917ae580f58f9bba4b3b3ca3321eaf78752ac9bc Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Thu, 7 May 2026 00:16:24 +0800 Subject: [PATCH 10/19] fix(dashboard): keep first home orb priority ordered --- apps/desktop/src/app/dashboard/DashboardHome.tsx | 6 ++++++ .../src/features/dashboard/dashboard.contract.test.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/apps/desktop/src/app/dashboard/DashboardHome.tsx b/apps/desktop/src/app/dashboard/DashboardHome.tsx index 607206e77..f81b602c1 100644 --- a/apps/desktop/src/app/dashboard/DashboardHome.tsx +++ b/apps/desktop/src/app/dashboard/DashboardHome.tsx @@ -59,6 +59,12 @@ function pickNextSummonIndex( return 0; } + // Keep the very first visible orb aligned with the service-side priority + // ordering so urgent overview signals are not randomized behind softer copy. + if (previousIndex < 0 || previousModule === null) { + return 0; + } + const candidateIndexes = templates .map((template, index) => ({ index, module: template.module })) .filter((candidate) => candidate.index !== previousIndex && candidate.module !== previousModule) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 696d18eff..d86cb3b70 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1787,6 +1787,8 @@ test("dashboard home randomizes summons while preferring a different module when const dashboardHomeSource = readFileSync(resolve(desktopRoot, "src/app/dashboard/DashboardHome.tsx"), "utf8"); assert.match(dashboardHomeSource, /function pickNextSummonIndex\(/); + assert.match(dashboardHomeSource, /if \(previousIndex < 0 \|\| previousModule === null\) \{/); + assert.match(dashboardHomeSource, /return 0;/); assert.match(dashboardHomeSource, /candidate\.module !== previousModule/); assert.match(dashboardHomeSource, /const pool = candidateIndexes\.length > 0 \? candidateIndexes : fallbackIndexes/); assert.match(dashboardHomeSource, /Math\.floor\(Math\.random\(\) \* pool\.length\)/); From 8177a17246ffb1d9486aa82d1b56f42a0d168775 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Thu, 7 May 2026 00:18:38 +0800 Subject: [PATCH 11/19] fix(dashboard): align home orb task CTA targets --- .../dashboard/dashboard.contract.test.ts | 77 +++++++++++++++++++ .../dashboard/home/dashboardHome.service.ts | 2 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index d86cb3b70..5febe5b43 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -6966,6 +6966,83 @@ test("dashboard home keeps a low-priority safety summon available when the forma ); }); +test("dashboard home only uses quick actions for task summons that can truly deep-link to task detail", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + summonTemplates: Array<{ module: string; nextStep?: string; stateKey: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + const taskSummon = data.summonTemplates.find((item) => item.stateKey === "task_working"); + assert.equal(taskSummon?.module, "tasks"); + assert.equal(taskSummon?.nextStep, "打开任务页"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + if (moduleName === "tasks") { + return { + highlights: ["继续推进当前摘要任务"], + module: moduleName, + summary: { + blocked_tasks: 0, + processing_tasks: 0, + waiting_auth_tasks: 0, + }, + tab: "focus", + }; + } + + return { + highlights: [], + module: moduleName, + summary: {}, + tab: "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: ["当前任务轨道已有新的系统摘要。"], + quick_actions: ["打开任务详情"], + trust_summary: { + has_restore_point: false, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [], + profile: null, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home prefers formal mirror references over profile copy when both exist", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 9a309601e..0cc07cd8e 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -270,7 +270,7 @@ function getOverviewQuickActions(overview: AgentDashboardOverviewGetResult) { } function getSummonNextStep(state: DashboardHomeStateData, quickActions?: string[]) { - if (state.module === "tasks" && quickActions?.[0]) { + if (state.module === "tasks" && state.navigationTarget?.kind === "task_detail" && quickActions?.[0]) { return quickActions[0]; } From 4e990bf9598c6389514156d54ad30657ea655af1 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Fri, 8 May 2026 01:34:22 +0800 Subject: [PATCH 12/19] fix(dashboard): humanize home mirror orb copy --- .../dashboard/dashboard.contract.test.ts | 8 +- .../home/components/DashboardEventPanel.tsx | 10 ++ .../dashboard/home/dashboardHome.service.ts | 109 ++++++++---------- .../dashboard/home/dashboardHome.types.ts | 7 ++ 4 files changed, 70 insertions(+), 64 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 5febe5b43..ecdeae930 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1779,6 +1779,8 @@ test("dashboard event panel routes task-detail actions through the shared naviga assert.match(panelSource, /navigateToDashboardTaskDetail/); assert.match(panelSource, /target\?\.kind === "task_detail"/); + assert.match(panelSource, /target\?\.kind === "mirror_detail"/); + assert.match(panelSource, /activeDetailKey: target\.activeDetailKey/); assert.match(panelSource, /resolvePrimaryActionLabel/); assert.match(panelSource, /activeState\.navigationTarget\?\.label/); }); @@ -6822,7 +6824,7 @@ test("dashboard home reuses formal mirror profile fields for memory copy", async const service = requireFn(modulePath) as { loadDashboardHomeData: () => Promise<{ - stateMap: Record }>; + stateMap: Record; navigationTarget?: { kind: string; activeDetailKey?: string } }>; }>; }; @@ -6831,6 +6833,8 @@ test("dashboard home reuses formal mirror profile fields for memory copy", async assert.equal(data.stateMap.memory_summary?.headline, "用户画像"); assert.equal(data.stateMap.memory_summary?.subline, "工作风格:偏好即时结果回显"); assert.equal(data.stateMap.memory_summary?.context?.[0]?.text, "后端画像字段 3 项"); + assert.equal(data.stateMap.memory_summary?.navigationTarget?.kind, "mirror_detail"); + assert.equal(data.stateMap.memory_summary?.navigationTarget?.activeDetailKey, "profile"); }, { getDashboardModule: async (params: unknown) => { @@ -7059,7 +7063,7 @@ test("dashboard home prefers formal mirror references over profile copy when bot assert.equal(data.stateMap.memory_habit?.headline, "近期被调用记忆"); assert.equal(data.stateMap.memory_habit?.subline, "本周战略复盘已被近期任务再次引用。"); - assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "最近记忆引用:memory_strategy_weekly"); + assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "本周战略复盘已被近期任务再次引用。"); }, { getDashboardModule: async (params: unknown) => ({ diff --git a/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx b/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx index 9352eabd6..94e0b4e17 100644 --- a/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx +++ b/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx @@ -116,6 +116,16 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state return; } + if (target?.kind === "mirror_detail") { + navigate(resolveDashboardModuleRoutePath("memory"), { + state: { + activeDetailKey: target.activeDetailKey, + focusMemoryId: target.focusMemoryId, + }, + }); + return; + } + navigate(resolveDashboardModuleRoutePath(target?.module ?? module)); }, 0); } diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 0cc07cd8e..3017ffd91 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -234,6 +234,20 @@ function buildModuleNavigationTarget( }; } +function buildMirrorDetailNavigationTarget( + activeDetailKey: "profile" | "memory" | "history", + label: string, + focusMemoryId?: string, +): DashboardHomeNavigationTarget { + return { + activeDetailKey, + focusMemoryId, + kind: "mirror_detail", + label, + module: "memory", + }; +} + function buildTaskDetailNavigationTarget(taskId: string, label = "打开任务详情"): DashboardHomeNavigationTarget { return { kind: "task_detail", @@ -631,19 +645,6 @@ function buildTaskState( return state; } -function buildNotesItems(recommendations: RecommendationItem[]): DashboardHomeNoteItem[] { - return recommendations - .filter((item) => inferModuleFromRecommendation(item) === "notes") - .slice(0, 3) - .map((item, index) => ({ - id: item.recommendation_id, - status: index === 0 ? "processing" : "pending", - tag: index === 0 ? "Agent 建议" : "待整理", - text: item.text, - time: index === 0 ? "现在" : "稍后", - })); -} - function mapHomeNotesFromBuckets(noteBuckets: DashboardHomeNoteBuckets) { return [ ...noteBuckets.upcoming, @@ -665,28 +666,24 @@ function mapHomeNotesFromBuckets(noteBuckets: DashboardHomeNoteBuckets) { } satisfies DashboardHomeNoteItem)); } -function buildNotesHeadline(noteItems: DashboardHomeNoteItem[], noteBuckets: DashboardHomeNoteBuckets | null) { +function buildNotesHeadline(noteBuckets: DashboardHomeNoteBuckets | null) { if (noteBuckets?.primaryItem) { return noteBuckets.primaryItem.item.title; } - if (noteItems.length > 0) { - return `近期便签 ${noteItems.length} 条待整理`; - } - return "这里还没有可协作的事项"; } -function buildNotesSubline(noteItems: DashboardHomeNoteItem[], exceptions: number) { - if (noteItems[0]) { - return noteItems[0].text; +function buildNotesSubline(noteBuckets: DashboardHomeNoteBuckets | null, exceptions: number) { + if (noteBuckets?.primaryItem) { + return describeNotePreview(noteBuckets.primaryItem.item, noteBuckets.primaryItem.experience); } - if (exceptions === 0) { - return "等你把想记住的事情交给便签协作后,这里会按近期要做、后续安排、重复事项和已结束四组方式整理出来。"; + if (exceptions > 0) { + return `当前例外项 ${exceptions} 条,建议优先整理最接近执行窗口的事项。`; } - return `当前例外项 ${exceptions} 条,建议优先整理最接近执行窗口的事项。`; + return "等你把想记住的事情交给便签协作后,这里会按近期要做、后续安排、重复事项和已结束四组方式整理出来。"; } function buildNotesContext(noteItems: DashboardHomeNoteItem[], highlights: string[], noteBuckets: DashboardHomeNoteBuckets | null) { @@ -710,26 +707,6 @@ function buildNotesContext(noteItems: DashboardHomeNoteItem[], highlights: strin ]; } - if (noteItems.length > 0) { - return [ - { - iconKey: "note", - text: `推荐事项 ${noteItems.length} 条`, - type: "active" as const, - }, - { - iconKey: "calendar", - text: noteItems[0]?.text ?? "打开便签页查看待处理事项。", - type: "hint" as const, - }, - { - iconKey: "repeat", - text: highlights[0] ?? "打开便签页查看正式状态。", - type: "normal" as const, - }, - ]; - } - return [ { iconKey: "note", @@ -747,16 +724,15 @@ function buildNotesContext(noteItems: DashboardHomeNoteItem[], highlights: strin function buildNotesState( stateKey: DashboardHomeEventStateKey, notesModule: AgentDashboardModuleGetResult, - recommendations: RecommendationItem[], noteBuckets: DashboardHomeNoteBuckets | null, ) { const state = cloneStateData(dashboardHomeStates[stateKey]); const highlights = getNotesHighlights(notesModule); - const noteItems = noteBuckets ? mapHomeNotesFromBuckets(noteBuckets) : buildNotesItems(recommendations); + const noteItems = noteBuckets ? mapHomeNotesFromBuckets(noteBuckets) : []; const exceptions = getModuleSummaryNumber(notesModule, "exceptions"); - state.headline = buildNotesHeadline(noteItems, noteBuckets); - state.subline = buildNotesSubline(noteItems, exceptions); + state.headline = buildNotesHeadline(noteBuckets); + state.subline = buildNotesSubline(noteBuckets, exceptions); state.context = buildNotesContext(noteItems, highlights, noteBuckets); state.notes = noteItems.length > 0 ? noteItems : state.notes; state.navigationTarget = buildModuleNavigationTarget("notes"); @@ -782,16 +758,19 @@ function buildMemoryInsights(memoryModule: AgentDashboardModuleGetResult): Dashb function buildFormalMirrorInsights(overview: AgentMirrorOverviewGetResult): DashboardHomeInsightItem[] { const latestReference = overview.memory_references[0] ?? null; if (latestReference) { + const summary = latestReference.summary || latestReference.reason || "最近有一条长期记忆再次命中当前协作。"; return [ { emphasis: true, iconKey: "brain", - text: latestReference.memory_id, - }, - { - iconKey: "time", - text: latestReference.summary || latestReference.reason, + text: summary, }, + ...(latestReference.reason && latestReference.reason !== summary + ? [{ + iconKey: "repeat", + text: latestReference.reason, + } satisfies DashboardHomeInsightItem] + : []), ]; } @@ -841,22 +820,26 @@ function buildMemorySubline(highlights: string[]) { function buildFormalMirrorState(overview: AgentMirrorOverviewGetResult) { const latestReference = overview.memory_references[0] ?? null; if (latestReference) { + const summary = latestReference.summary || latestReference.reason || "最近有一条长期记忆再次命中当前协作。"; return { context: [ { iconKey: "brain", - text: `最近记忆引用:${latestReference.memory_id}`, + text: summary, type: "active" as const, }, - { - iconKey: "repeat", - text: latestReference.summary || latestReference.reason, - type: "hint" as const, - }, + ...(latestReference.reason && latestReference.reason !== summary + ? [{ + iconKey: "repeat", + text: latestReference.reason, + type: "hint" as const, + }] + : []), ], headline: "近期被调用记忆", insights: buildFormalMirrorInsights(overview), - subline: latestReference.summary || latestReference.reason, + navigationTarget: buildMirrorDetailNavigationTarget("memory", "打开镜子页", latestReference.memory_id), + subline: summary, }; } @@ -882,6 +865,7 @@ function buildFormalMirrorState(overview: AgentMirrorOverviewGetResult) { ], headline: "用户画像", insights: buildFormalMirrorInsights(overview), + navigationTarget: buildMirrorDetailNavigationTarget("profile", "打开镜子页"), subline: `工作风格:${profile.work_style}`, }; } @@ -897,6 +881,7 @@ function buildFormalMirrorState(overview: AgentMirrorOverviewGetResult) { ], headline: "历史概要", insights: buildFormalMirrorInsights(overview), + navigationTarget: buildMirrorDetailNavigationTarget("history", "打开镜子页"), subline: overview.history_summary[1] ?? overview.history_summary[0], }; } @@ -917,7 +902,7 @@ function buildMemoryState( state.subline = formalMirrorState.subline; state.insights = formalMirrorState.insights; state.context = formalMirrorState.context; - state.navigationTarget = buildModuleNavigationTarget("memory"); + state.navigationTarget = formalMirrorState.navigationTarget; return state; } } @@ -1250,7 +1235,7 @@ function buildDashboardHomeData(input: { }; stateMap[stateKeys.tasks] = buildTaskState(stateKeys.tasks, input.overview, input.moduleResults.tasks); - stateMap[stateKeys.notes] = buildNotesState(stateKeys.notes, input.moduleResults.notes, input.recommendations.items, input.noteBuckets); + stateMap[stateKeys.notes] = buildNotesState(stateKeys.notes, input.moduleResults.notes, input.noteBuckets); stateMap[stateKeys.memory] = buildMemoryState(stateKeys.memory, input.moduleResults.memory, input.mirrorOverview); stateMap[stateKeys.safety] = buildSafetyState(stateKeys.safety, input.overview, input.moduleResults.safety); diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts index c1969a7f7..82dc1848b 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts @@ -71,6 +71,13 @@ export type DashboardHomeNavigationTarget = label: string; module: "tasks"; taskId: string; + } + | { + kind: "mirror_detail"; + label: string; + module: "memory"; + activeDetailKey: "profile" | "memory" | "history"; + focusMemoryId?: string; }; export type DashboardHomeStateData = { From 3bfe49dfe995c836abfaa06218c49b7e5e53ab9b Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Fri, 8 May 2026 01:54:21 +0800 Subject: [PATCH 13/19] fix(dashboard): rotate home mirror orb sources --- .../src/app/dashboard/DashboardHome.tsx | 22 +- .../dashboard/dashboard.contract.test.ts | 80 ++++++ .../home/components/DashboardEventOrb.tsx | 6 +- .../dashboard/home/dashboardHome.service.ts | 263 ++++++++++-------- .../dashboard/home/dashboardHome.types.ts | 1 + 5 files changed, 255 insertions(+), 117 deletions(-) diff --git a/apps/desktop/src/app/dashboard/DashboardHome.tsx b/apps/desktop/src/app/dashboard/DashboardHome.tsx index f81b602c1..bea8e5d77 100644 --- a/apps/desktop/src/app/dashboard/DashboardHome.tsx +++ b/apps/desktop/src/app/dashboard/DashboardHome.tsx @@ -102,13 +102,14 @@ export function DashboardHome({ const [orbDragOffset, setOrbDragOffset] = useState({ x: 0, y: 0 }); const [hoveredEntranceKey, setHoveredEntranceKey] = useState(null); const [activeStateKey, setActiveStateKey] = useState(null); + const [activeExpandedState, setActiveExpandedState] = useState(null); const [summons, setSummons] = useState([]); const summonIdRef = useRef(0); const lastSummonIndexRef = useRef(-1); const lastSummonModuleRef = useRef(null); const summonTimerRef = useRef(null); - const activeState = activeStateKey ? data.stateMap[activeStateKey] : null; + const activeState = activeExpandedState ?? (activeStateKey ? data.stateMap[activeStateKey] : null); const activeModule = hoveredEntranceKey ? dashboardEntranceOrbs.find((config) => config.key === hoveredEntranceKey)?.module ?? activeState?.module ?? null : activeState?.module ?? null; @@ -304,8 +305,9 @@ export function DashboardHome({ onRecommendationFeedback?.(event.recommendationId, "negative"); } }} - onExpand={(stateKey) => { - setActiveStateKey(stateKey); + onExpand={(expandedEvent) => { + setActiveStateKey(expandedEvent.stateKey); + setActiveExpandedState(expandedEvent.expandedState ?? null); if (event.recommendationId) { onRecommendationFeedback?.(event.recommendationId, "positive"); } @@ -329,7 +331,19 @@ export function DashboardHome({
- setActiveStateKey(null)} onStateChange={setActiveStateKey} stateGroups={data.stateGroups} stateMap={data.stateMap} /> + { + setActiveExpandedState(null); + setActiveStateKey(null); + }} + onStateChange={(stateKey) => { + setActiveExpandedState(null); + setActiveStateKey(stateKey); + }} + stateGroups={data.stateGroups} + stateMap={data.stateMap} + /> ); } diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index ecdeae930..a72331695 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -7118,6 +7118,86 @@ test("dashboard home prefers formal mirror references over profile copy when bot ); }); +test("dashboard home rotates mirror summons across formal memory, profile, and history sections", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + summonTemplates: Array<{ module: string; message: string; nextStep?: string; expandedState?: { headline: string; navigationTarget?: { kind: string; activeDetailKey?: string } } }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + const mirrorSummons = data.summonTemplates.filter((item) => item.module === "memory"); + + assert.deepEqual( + mirrorSummons.slice(0, 3).map((item) => item.expandedState?.headline), + ["近期被调用记忆", "用户画像", "历史概要"], + ); + assert.deepEqual( + mirrorSummons.slice(0, 3).map((item) => item.expandedState?.navigationTarget?.activeDetailKey), + ["memory", "profile", "history"], + ); + assert.equal(mirrorSummons[0]?.message, "近期被调用记忆"); + assert.equal(mirrorSummons[1]?.message, "用户画像"); + assert.equal(mirrorSummons[2]?.message, "历史概要"); + }, + { + getDashboardModule: async (params: unknown) => ({ + highlights: [], + module: (params as { module?: string }).module ?? "unknown", + summary: {}, + tab: "overview", + }), + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: false, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: ["这里是最近一条历史概要。", "第二条历史概要。"], + memory_references: [ + { + memory_id: "memory_strategy_weekly", + reason: "近期任务再次命中这段长期记忆。", + summary: "本周战略复盘已被近期任务再次引用。", + }, + ], + profile: { + active_hours: "16-21h", + preferred_output: "bubble", + work_style: "偏好即时结果回显", + }, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home keeps notes copy module-native and skips fake empty-note summons", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/components/DashboardEventOrb.tsx b/apps/desktop/src/features/dashboard/home/components/DashboardEventOrb.tsx index f799ee275..f669341b9 100644 --- a/apps/desktop/src/features/dashboard/home/components/DashboardEventOrb.tsx +++ b/apps/desktop/src/features/dashboard/home/components/DashboardEventOrb.tsx @@ -6,7 +6,7 @@ type DashboardEventOrbProps = { event: DashboardHomeSummonEvent; stateMap: Record; onDismiss: (id: string) => void; - onExpand: (stateKey: DashboardHomeSummonEvent["stateKey"]) => void; + onExpand: (event: DashboardHomeSummonEvent) => void; }; type Phase = "dormant" | "emerging" | "present" | "receding" | "gone"; @@ -237,9 +237,9 @@ export function DashboardEventOrb({ event, stateMap, onDismiss, onExpand }: Dash return; } - onExpand(event.stateKey); + onExpand(event); startRecede(); - }, [event.stateKey, isDragging, onExpand, startRecede]); + }, [event, isDragging, onExpand, startRecede]); if (phase === "gone") { return null; diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 3017ffd91..e06ad7954 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -755,54 +755,129 @@ function buildMemoryInsights(memoryModule: AgentDashboardModuleGetResult): Dashb })); } -function buildFormalMirrorInsights(overview: AgentMirrorOverviewGetResult): DashboardHomeInsightItem[] { +function createFormalMirrorStateBase(stateKey: DashboardHomeEventStateKey) { + return cloneStateData(dashboardHomeStates[stateKey]); +} + +function buildReferenceMirrorState( + stateKey: DashboardHomeEventStateKey, + overview: AgentMirrorOverviewGetResult, +) { const latestReference = overview.memory_references[0] ?? null; - if (latestReference) { - const summary = latestReference.summary || latestReference.reason || "最近有一条长期记忆再次命中当前协作。"; - return [ - { - emphasis: true, - iconKey: "brain", - text: summary, - }, - ...(latestReference.reason && latestReference.reason !== summary - ? [{ - iconKey: "repeat", - text: latestReference.reason, - } satisfies DashboardHomeInsightItem] - : []), - ]; + if (!latestReference) { + return null; } - if (overview.profile) { - return [ - { - emphasis: true, - iconKey: "brain", - text: `工作风格:${overview.profile.work_style}`, - }, - { - iconKey: "chat", - text: `偏好交付:${overview.profile.preferred_output}`, - }, - { - iconKey: "time", - text: `活跃时段:${overview.profile.active_hours}`, - }, - ]; + const state = createFormalMirrorStateBase(stateKey); + const summary = latestReference.summary || latestReference.reason || "最近有一条长期记忆再次命中当前协作。"; + state.headline = "近期被调用记忆"; + state.subline = summary; + state.context = [ + { + iconKey: "brain", + text: summary, + type: "active", + }, + ...(latestReference.reason && latestReference.reason !== summary + ? [{ + iconKey: "repeat", + text: latestReference.reason, + type: "hint", + } satisfies DashboardHomeContextItem] + : []), + ]; + state.insights = [ + { + emphasis: true, + iconKey: "brain", + text: summary, + }, + ...(latestReference.reason && latestReference.reason !== summary + ? [{ + iconKey: "repeat", + text: latestReference.reason, + } satisfies DashboardHomeInsightItem] + : []), + ]; + state.navigationTarget = buildMirrorDetailNavigationTarget("memory", "打开镜子页", latestReference.memory_id); + return state; +} + +function buildProfileMirrorState( + stateKey: DashboardHomeEventStateKey, + overview: AgentMirrorOverviewGetResult, +) { + const profile = overview.profile; + if (!profile) { + return null; } - if (overview.history_summary[0]) { - return [ - { - emphasis: true, - iconKey: "repeat", - text: overview.history_summary[0], - }, - ]; + const state = createFormalMirrorStateBase(stateKey); + state.headline = "用户画像"; + state.subline = `工作风格:${profile.work_style}`; + state.context = [ + { + iconKey: "brain", + text: "后端画像字段 3 项", + type: "active", + }, + { + iconKey: "chat", + text: `偏好交付:${profile.preferred_output}`, + type: "normal", + }, + { + iconKey: "time", + text: `活跃时段:${profile.active_hours}`, + type: "hint", + }, + ]; + state.insights = [ + { + emphasis: true, + iconKey: "brain", + text: `工作风格:${profile.work_style}`, + }, + { + iconKey: "chat", + text: `偏好交付:${profile.preferred_output}`, + }, + { + iconKey: "time", + text: `活跃时段:${profile.active_hours}`, + }, + ]; + state.navigationTarget = buildMirrorDetailNavigationTarget("profile", "打开镜子页"); + return state; +} + +function buildHistoryMirrorState( + stateKey: DashboardHomeEventStateKey, + overview: AgentMirrorOverviewGetResult, +) { + if (!overview.history_summary[0]) { + return null; } - return []; + const state = createFormalMirrorStateBase(stateKey); + state.headline = "历史概要"; + state.subline = overview.history_summary[1] ?? overview.history_summary[0]; + state.context = [ + { + iconKey: "repeat", + text: overview.history_summary[0], + type: "active", + }, + ]; + state.insights = [ + { + emphasis: true, + iconKey: "repeat", + text: overview.history_summary[0], + }, + ]; + state.navigationTarget = buildMirrorDetailNavigationTarget("history", "打开镜子页"); + return state; } function buildMemoryHeadline(highlights: string[]) { @@ -817,76 +892,43 @@ function buildMemorySubline(highlights: string[]) { return highlights[0] ?? "镜子会持续整理近期协作节奏和重复出现的模式。"; } -function buildFormalMirrorState(overview: AgentMirrorOverviewGetResult) { - const latestReference = overview.memory_references[0] ?? null; - if (latestReference) { - const summary = latestReference.summary || latestReference.reason || "最近有一条长期记忆再次命中当前协作。"; - return { - context: [ - { - iconKey: "brain", - text: summary, - type: "active" as const, - }, - ...(latestReference.reason && latestReference.reason !== summary - ? [{ - iconKey: "repeat", - text: latestReference.reason, - type: "hint" as const, - }] - : []), - ], - headline: "近期被调用记忆", - insights: buildFormalMirrorInsights(overview), - navigationTarget: buildMirrorDetailNavigationTarget("memory", "打开镜子页", latestReference.memory_id), - subline: summary, - }; - } +function buildPreferredFormalMirrorState( + stateKey: DashboardHomeEventStateKey, + overview: AgentMirrorOverviewGetResult, +) { + return ( + buildReferenceMirrorState(stateKey, overview) ?? + buildProfileMirrorState(stateKey, overview) ?? + buildHistoryMirrorState(stateKey, overview) + ); +} - const profile = overview.profile; - if (profile) { - return { - context: [ - { - iconKey: "brain", - text: "后端画像字段 3 项", - type: "active" as const, - }, - { - iconKey: "chat", - text: `偏好交付:${profile.preferred_output}`, - type: "normal" as const, - }, - { - iconKey: "time", - text: `活跃时段:${profile.active_hours}`, - type: "hint" as const, - }, - ], - headline: "用户画像", - insights: buildFormalMirrorInsights(overview), - navigationTarget: buildMirrorDetailNavigationTarget("profile", "打开镜子页"), - subline: `工作风格:${profile.work_style}`, - }; - } +function buildFormalMirrorSummons( + stateKey: DashboardHomeEventStateKey, + overview: AgentMirrorOverviewGetResult, +): Array> { + const templates = [ + buildReferenceMirrorState(stateKey, overview), + buildProfileMirrorState(stateKey, overview), + buildHistoryMirrorState(stateKey, overview), + ].flatMap((state) => { + if (!state) { + return []; + } - if (overview.history_summary[0]) { - return { - context: [ - { - iconKey: "repeat", - text: overview.history_summary[0], - type: "active" as const, - }, - ], - headline: "历史概要", - insights: buildFormalMirrorInsights(overview), - navigationTarget: buildMirrorDetailNavigationTarget("history", "打开镜子页"), - subline: overview.history_summary[1] ?? overview.history_summary[0], - }; - } + return [{ + duration: 5_600, + expandedState: state, + message: state.headline, + module: "memory", + nextStep: state.navigationTarget?.label ?? dashboardModuleNextSteps.memory, + priority: "low", + reason: state.subline, + stateKey: state.key, + } satisfies Omit]; + }); - return null; + return dedupeSummonTemplates(templates); } function buildMemoryState( @@ -896,7 +938,7 @@ function buildMemoryState( ) { const state = cloneStateData(dashboardHomeStates[stateKey]); if (mirrorOverview) { - const formalMirrorState = buildFormalMirrorState(mirrorOverview); + const formalMirrorState = buildPreferredFormalMirrorState(stateKey, mirrorOverview); if (formalMirrorState) { state.headline = formalMirrorState.headline; state.subline = formalMirrorState.subline; @@ -1240,9 +1282,10 @@ function buildDashboardHomeData(input: { stateMap[stateKeys.safety] = buildSafetyState(stateKeys.safety, input.overview, input.moduleResults.safety); const overviewSummons = buildOverviewSummons(input.overview, stateKeys, stateMap); + const mirrorSummons = input.mirrorOverview ? buildFormalMirrorSummons(stateKeys.memory, input.mirrorOverview) : []; const moduleSummons = buildModuleSummarySummons(stateKeys, stateMap); const recommendationSummons = buildRecommendationSummons(input.recommendations.items, stateKeys, input.moduleResults); - const summonTemplates = dedupeSummonTemplates([...overviewSummons, ...moduleSummons, ...recommendationSummons]); + const summonTemplates = dedupeSummonTemplates([...overviewSummons, ...mirrorSummons, ...moduleSummons, ...recommendationSummons]); return { focusLine: buildFocusLine(input.overview, input.moduleResults.tasks, summonTemplates), diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts index 82dc1848b..c1635a336 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.types.ts @@ -129,6 +129,7 @@ export type DashboardHomeStateGroup = { export type DashboardHomeSummonEvent = { copyDuration?: number; + expandedState?: DashboardHomeStateData; id: string; hideCopy?: boolean; module: DashboardHomeModuleKey; From 1631f025428c442a0f0539ccdd84b18b9b758329 Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Fri, 8 May 2026 18:48:24 +0800 Subject: [PATCH 14/19] fix(dashboard): dedupe home event panel copy --- .../dashboard/dashboard.contract.test.ts | 10 +- .../home/components/DashboardEventPanel.tsx | 134 +++++++++++++++--- .../dashboard/home/dashboardHome.service.ts | 130 ++++++++--------- 3 files changed, 182 insertions(+), 92 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index a72331695..6afd72817 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1783,6 +1783,10 @@ test("dashboard event panel routes task-detail actions through the shared naviga assert.match(panelSource, /activeDetailKey: target\.activeDetailKey/); assert.match(panelSource, /resolvePrimaryActionLabel/); assert.match(panelSource, /activeState\.navigationTarget\?\.label/); + assert.match(panelSource, /filterDistinctContextItems/); + assert.match(panelSource, /filterDistinctSignals/); + assert.match(panelSource, /buildMetaPills/); + assert.doesNotMatch(panelSource, /这是首页事件舱/); }); test("dashboard home randomizes summons while preferring a different module when alternatives exist", () => { @@ -6832,7 +6836,8 @@ test("dashboard home reuses formal mirror profile fields for memory copy", async assert.equal(data.stateMap.memory_summary?.headline, "用户画像"); assert.equal(data.stateMap.memory_summary?.subline, "工作风格:偏好即时结果回显"); - assert.equal(data.stateMap.memory_summary?.context?.[0]?.text, "后端画像字段 3 项"); + assert.equal(data.stateMap.memory_summary?.context?.[0]?.text, "偏好交付:bubble"); + assert.equal(data.stateMap.memory_summary?.context?.[1]?.text, "活跃时段:16-21h"); assert.equal(data.stateMap.memory_summary?.navigationTarget?.kind, "mirror_detail"); assert.equal(data.stateMap.memory_summary?.navigationTarget?.activeDetailKey, "profile"); }, @@ -7063,7 +7068,8 @@ test("dashboard home prefers formal mirror references over profile copy when bot assert.equal(data.stateMap.memory_habit?.headline, "近期被调用记忆"); assert.equal(data.stateMap.memory_habit?.subline, "本周战略复盘已被近期任务再次引用。"); - assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "本周战略复盘已被近期任务再次引用。"); + assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "记忆 ID:memory_strategy_weekly"); + assert.equal(data.stateMap.memory_habit?.context?.[1]?.text, "命中内容:本周战略复盘已被近期任务再次引用。"); }, { getDashboardModule: async (params: unknown) => ({ diff --git a/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx b/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx index 94e0b4e17..f6de1efff 100644 --- a/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx +++ b/apps/desktop/src/features/dashboard/home/components/DashboardEventPanel.tsx @@ -49,6 +49,75 @@ const contextIcons = { user: UserRound, }; +function normalizePanelCopy(text: string) { + return text.trim().replace(/\s+/g, " "); +} + +function buildPanelCopyRegistry(activeState: DashboardHomeStateData) { + return new Set( + [activeState.headline, activeState.subline] + .filter((text): text is string => typeof text === "string") + .map((text) => normalizePanelCopy(text)) + .filter(Boolean), + ); +} + +function filterDistinctContextItems(items: DashboardHomeContextItem[], registry: Set) { + return items.filter((item) => { + const normalized = normalizePanelCopy(item.text); + if (normalized === "" || registry.has(normalized)) { + return false; + } + + registry.add(normalized); + return true; + }); +} + +function filterDistinctInsights( + insights: NonNullable, + registry: Set, +) { + return insights.filter((insight) => { + const normalized = normalizePanelCopy(insight.text); + if (normalized === "" || registry.has(normalized)) { + return false; + } + + registry.add(normalized); + return true; + }); +} + +function filterDistinctSignals(signals: DashboardHomeSignalItem[], registry: Set) { + return signals.filter((signal) => { + const normalized = normalizePanelCopy(`${signal.label} ${signal.value} ${signal.translation ?? ""}`); + if (normalized === "" || registry.has(normalized)) { + return false; + } + + registry.add(normalized); + return true; + }); +} + +function buildMetaPills(activeState: DashboardHomeStateData, moduleLabel: string | undefined) { + const candidates = [activeState.tag, activeState.progressLabel, moduleLabel].filter( + (item): item is string => typeof item === "string" && item.trim() !== "", + ); + const seen = new Set(); + + return candidates.filter((item) => { + const normalized = normalizePanelCopy(item); + if (seen.has(normalized)) { + return false; + } + + seen.add(normalized); + return true; + }); +} + function renderContext(items: DashboardHomeContextItem[]) { return items.map((item) => (
@@ -138,6 +207,35 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state return stateGroups.find((group) => group.key === activeState.module)?.states ?? []; }, [activeState, stateGroups]); + const moduleLabel = useMemo(() => { + if (!activeState) { + return undefined; + } + + return stateGroups.find((group) => group.key === activeState.module)?.label; + }, [activeState, stateGroups]); + + const metaPills = useMemo(() => { + if (!activeState) { + return []; + } + + return buildMetaPills(activeState, moduleLabel); + }, [activeState, moduleLabel]); + + const sectionData = useMemo(() => { + if (!activeState) { + return null; + } + + const registry = buildPanelCopyRegistry(activeState); + return { + context: filterDistinctContextItems(activeState.context, registry), + insights: activeState.insights ? filterDistinctInsights(activeState.insights, registry) : [], + signals: activeState.signals ? filterDistinctSignals(activeState.signals, registry) : [], + }; + }, [activeState]); + return ( {activeState ? ( @@ -170,11 +268,15 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state
- - {activeState.tag} - - {activeState.progressLabel ? {activeState.progressLabel} : null} - {stateGroups.find((group) => group.key === activeState.module)?.label} + {metaPills.map((pill, index) => ( + + {pill} + + ))}
{moduleStates.length > 1 ? ( @@ -205,10 +307,12 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state ) : null}
-
-

当前上下文

-
{renderContext(activeState.context)}
-
+ {sectionData && sectionData.context.length > 0 ? ( +
+

当前上下文

+
{renderContext(sectionData.context)}
+
+ ) : null} {activeState.notes?.length ? (
@@ -232,11 +336,11 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state
) : null} - {activeState.insights?.length ? ( + {sectionData && sectionData.insights.length > 0 ? (

镜子观察

- {activeState.insights.map((insight) => ( + {sectionData.insights.map((insight) => (
{(() => { @@ -253,10 +357,10 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state
) : null} - {activeState.signals?.length ? ( + {sectionData && sectionData.signals.length > 0 ? (

边界摘要

-
{renderSignalCards(activeState.signals)}
+
{renderSignalCards(sectionData.signals)}
) : null} @@ -287,10 +391,6 @@ export function DashboardEventPanel({ activeState, onClose, onStateChange, state {resolvePrimaryActionLabel(activeState)} -
- - 这是首页事件舱,点击主按钮可进入对应子页面 -
diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index e06ad7954..064bb3247 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -291,6 +291,27 @@ function getSummonNextStep(state: DashboardHomeStateData, quickActions?: string[ return state.navigationTarget?.label ?? dashboardModuleNextSteps[state.module]; } +function appendDistinctContextItem( + items: DashboardHomeContextItem[], + candidate: DashboardHomeContextItem | null, +) { + if (!candidate) { + return items; + } + + const normalizedCandidate = candidate.text.trim(); + if (normalizedCandidate === "") { + return items; + } + + if (items.some((item) => item.text.trim() === normalizedCandidate)) { + return items; + } + + items.push(candidate); + return items; +} + function dedupeSummonTemplates(templates: Array>) { const seen = new Set(); @@ -774,31 +795,17 @@ function buildReferenceMirrorState( state.subline = summary; state.context = [ { - iconKey: "brain", - text: summary, - type: "active", + iconKey: "link", + text: `记忆 ID:${latestReference.memory_id}`, + type: "hint", }, - ...(latestReference.reason && latestReference.reason !== summary - ? [{ - iconKey: "repeat", - text: latestReference.reason, - type: "hint", - } satisfies DashboardHomeContextItem] - : []), - ]; - state.insights = [ { - emphasis: true, iconKey: "brain", - text: summary, + text: `命中内容:${summary}`, + type: "active", }, - ...(latestReference.reason && latestReference.reason !== summary - ? [{ - iconKey: "repeat", - text: latestReference.reason, - } satisfies DashboardHomeInsightItem] - : []), ]; + state.insights = undefined; state.navigationTarget = buildMirrorDetailNavigationTarget("memory", "打开镜子页", latestReference.memory_id); return state; } @@ -816,11 +823,6 @@ function buildProfileMirrorState( state.headline = "用户画像"; state.subline = `工作风格:${profile.work_style}`; state.context = [ - { - iconKey: "brain", - text: "后端画像字段 3 项", - type: "active", - }, { iconKey: "chat", text: `偏好交付:${profile.preferred_output}`, @@ -832,21 +834,7 @@ function buildProfileMirrorState( type: "hint", }, ]; - state.insights = [ - { - emphasis: true, - iconKey: "brain", - text: `工作风格:${profile.work_style}`, - }, - { - iconKey: "chat", - text: `偏好交付:${profile.preferred_output}`, - }, - { - iconKey: "time", - text: `活跃时段:${profile.active_hours}`, - }, - ]; + state.insights = undefined; state.navigationTarget = buildMirrorDetailNavigationTarget("profile", "打开镜子页"); return state; } @@ -861,21 +849,13 @@ function buildHistoryMirrorState( const state = createFormalMirrorStateBase(stateKey); state.headline = "历史概要"; - state.subline = overview.history_summary[1] ?? overview.history_summary[0]; - state.context = [ - { - iconKey: "repeat", - text: overview.history_summary[0], - type: "active", - }, - ]; - state.insights = [ - { - emphasis: true, - iconKey: "repeat", - text: overview.history_summary[0], - }, - ]; + state.subline = overview.history_summary[0]; + state.context = overview.history_summary.slice(1, 3).map((summary, index) => ({ + iconKey: index === 0 ? "repeat" : "time", + text: summary, + type: "hint", + })); + state.insights = undefined; state.navigationTarget = buildMirrorDetailNavigationTarget("history", "打开镜子页"); return state; } @@ -983,23 +963,6 @@ function buildSafetyState(stateKey: DashboardHomeEventStateKey, overview: AgentD (trustSummary.pending_authorizations > 0 ? "建议先处理待授权操作,再继续推进其它任务。" : `工作区位于 ${trustSummary.workspace_path || "当前默认目录"}。`); - state.context = [ - { - iconKey: "shield", - text: `风险等级:${riskLabel}`, - type: trustSummary.risk_level === "green" ? "normal" : "warn", - }, - { - iconKey: "lock", - text: `待授权:${trustSummary.pending_authorizations} 项`, - type: trustSummary.pending_authorizations > 0 ? "warn" : "normal", - }, - { - iconKey: "history", - text: trustSummary.has_restore_point ? "最近恢复点可用" : "当前还没有恢复点", - type: trustSummary.has_restore_point ? "hint" : "warn", - }, - ]; state.signals = [ { iconKey: "shield", @@ -1023,11 +986,32 @@ function buildSafetyState(stateKey: DashboardHomeEventStateKey, overview: AgentD value: trustSummary.has_restore_point ? "可用" : "暂无", }, ]; + state.context = []; + appendDistinctContextItem( + state.context, + highlights[0] + ? { + iconKey: trustSummary.pending_authorizations > 0 ? "lock" : "shield", + text: highlights[0], + type: trustSummary.pending_authorizations > 0 ? "warn" : "hint", + } + : null, + ); + appendDistinctContextItem( + state.context, + trustSummary.workspace_path + ? { + iconKey: "file", + text: `工作区:${trustSummary.workspace_path}`, + type: "hint", + } + : null, + ); if (trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green") { state.anomaly = { actionLabel: "查看安全详情", - desc: highlights[0] ?? "当前存在需要优先确认的安全事项。", + desc: trustSummary.pending_authorizations > 0 ? "先处理待授权操作,再继续推进其它任务。" : "建议先确认当前风险边界。", dismissLabel: "稍后再看", severity: trustSummary.pending_authorizations > 0 ? "error" : "warn", title: trustSummary.pending_authorizations > 0 ? "安全链路有待处理项" : "当前需要留意执行边界", From e6cd46c308bd627a71756f653c16415efc7567ba Mon Sep 17 00:00:00 2001 From: gdemonc <1502689916@qq.com> Date: Sat, 9 May 2026 13:44:29 +0800 Subject: [PATCH 15/19] fix(dashboard): close expanded home panel on escape --- apps/desktop/src/app/dashboard/DashboardHome.tsx | 16 +++++++++------- .../dashboard/dashboard.contract.test.ts | 3 +++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/app/dashboard/DashboardHome.tsx b/apps/desktop/src/app/dashboard/DashboardHome.tsx index bea8e5d77..ee760c0f5 100644 --- a/apps/desktop/src/app/dashboard/DashboardHome.tsx +++ b/apps/desktop/src/app/dashboard/DashboardHome.tsx @@ -118,6 +118,11 @@ export function DashboardHome({ const currentReasonLine = activeState?.subline ?? summons[0]?.reason ?? data.focusLine.reason; const isOverlayOpen = Boolean(activeState || voiceOpen); + const closeActiveOverlay = useCallback(() => { + setActiveExpandedState(null); + setActiveStateKey(null); + }, []); + const scheduleSummon = useCallback(() => { if (data.summonTemplates.length === 0) { return; @@ -179,15 +184,15 @@ export function DashboardHome({ return; } - if (event.key === "Escape" && activeStateKey) { + if (event.key === "Escape" && (activeStateKey || activeExpandedState)) { event.preventDefault(); - setActiveStateKey(null); + closeActiveOverlay(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [activeStateKey]); + }, [activeExpandedState, activeStateKey, closeActiveOverlay]); const centerVisualState = voiceOpen ? "voice_locked" : getCenterState(activeStateKey); const pageStyle = { @@ -333,10 +338,7 @@ export function DashboardHome({ { - setActiveExpandedState(null); - setActiveStateKey(null); - }} + onClose={closeActiveOverlay} onStateChange={(stateKey) => { setActiveExpandedState(null); setActiveStateKey(stateKey); diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 6afd72817..616fe5884 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1799,6 +1799,9 @@ test("dashboard home randomizes summons while preferring a different module when assert.match(dashboardHomeSource, /const pool = candidateIndexes\.length > 0 \? candidateIndexes : fallbackIndexes/); assert.match(dashboardHomeSource, /Math\.floor\(Math\.random\(\) \* pool\.length\)/); assert.match(dashboardHomeSource, /lastSummonModuleRef\.current = template\.module/); + assert.match(dashboardHomeSource, /const closeActiveOverlay = useCallback\(\(\) => \{/); + assert.match(dashboardHomeSource, /if \(event\.key === "Escape" && \(activeStateKey \|\| activeExpandedState\)\) \{/); + assert.match(dashboardHomeSource, /onClose=\{closeActiveOverlay\}/); }); test("mirror page stays RPC-only instead of exposing a page-level mock toggle", () => { From 22190f280f6e633261047c719c6c186c9d254559 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Sun, 10 May 2026 05:01:41 +0000 Subject: [PATCH 16/19] fix(dashboard): align orb CTA with task route Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: gdemonc <146809967+gdemonc@users.noreply.github.com> --- .../dashboard/dashboard.contract.test.ts | 97 +++++++++++++++++++ .../dashboard/home/dashboardHome.service.ts | 11 ++- 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 616fe5884..a3c254a06 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -7055,6 +7055,103 @@ test("dashboard home only uses quick actions for task summons that can truly dee ); }); +test("dashboard home keeps task-detail CTA copy when the first quick action targets a different route", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record; + summonTemplates: Array<{ message: string; module: string; nextStep?: string; stateKey: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + const taskSummon = data.summonTemplates.find((item) => item.stateKey === "task_working"); + assert.equal(taskSummon?.module, "tasks"); + assert.equal(taskSummon?.message, "整理 Q3 复盘要点"); + assert.equal(taskSummon?.nextStep, "打开任务详情"); + assert.equal(data.stateMap.task_working?.navigationTarget?.kind, "task_detail"); + assert.equal(data.stateMap.task_working?.navigationTarget?.taskId, "task_focus_001"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + + if (moduleName === "tasks") { + return { + highlights: ["继续推进当前摘要任务"], + module: moduleName, + summary: { + blocked_tasks: 0, + focus_runtime_summary: { + active_steering_count: 0, + events_count: 1, + latest_event_type: null, + loop_stop_reason: null, + observation_signals: [], + }, + focus_task_id: "task_focus_001", + processing_tasks: 1, + waiting_auth_tasks: 1, + }, + tab: "focus", + }; + } + + return { + highlights: [], + module: moduleName, + summary: {}, + tab: "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: { + current_step: "生成摘要", + next_action: "等待处理完成", + status: "processing", + task_id: "task_focus_001", + title: "整理 Q3 复盘要点", + updated_at: "2026-04-07T10:40:00+08:00", + }, + high_value_signal: ["当前有 1 项操作等待授权"], + quick_actions: ["处理待授权操作", "打开任务详情"], + trust_summary: { + has_restore_point: true, + pending_authorizations: 1, + risk_level: "yellow", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [], + profile: null, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home prefers formal mirror references over profile copy when both exist", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 064bb3247..dd36bc78f 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -283,8 +283,17 @@ function getOverviewQuickActions(overview: AgentDashboardOverviewGetResult) { : []; } +function matchesNavigationActionLabel(state: DashboardHomeStateData, actionLabel: string) { + return state.navigationTarget?.label.trim() === actionLabel.trim(); +} + function getSummonNextStep(state: DashboardHomeStateData, quickActions?: string[]) { - if (state.module === "tasks" && state.navigationTarget?.kind === "task_detail" && quickActions?.[0]) { + if ( + state.module === "tasks" && + state.navigationTarget?.kind === "task_detail" && + quickActions?.[0] && + matchesNavigationActionLabel(state, quickActions[0]) + ) { return quickActions[0]; } From 575501e099b5ea9d8e47aeb64b3f35b0f8244eca Mon Sep 17 00:00:00 2001 From: xgopilot Date: Sun, 10 May 2026 05:22:11 +0000 Subject: [PATCH 17/19] fix(dashboard): sanitize mirror home orb copy Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: gdemonc <146809967+gdemonc@users.noreply.github.com> --- .../dashboard/dashboard.contract.test.ts | 71 ++++++++++++++++++- .../dashboard/home/dashboardHome.service.ts | 47 ++++++++---- 2 files changed, 103 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index a3c254a06..654a68801 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -7168,8 +7168,8 @@ test("dashboard home prefers formal mirror references over profile copy when bot assert.equal(data.stateMap.memory_habit?.headline, "近期被调用记忆"); assert.equal(data.stateMap.memory_habit?.subline, "本周战略复盘已被近期任务再次引用。"); - assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "记忆 ID:memory_strategy_weekly"); - assert.equal(data.stateMap.memory_habit?.context?.[1]?.text, "命中内容:本周战略复盘已被近期任务再次引用。"); + assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "来源:近期长期记忆命中"); + assert.equal(data.stateMap.memory_habit?.context?.[1]?.text, "近期任务再次命中这段长期记忆。"); }, { getDashboardModule: async (params: unknown) => ({ @@ -7224,6 +7224,73 @@ test("dashboard home prefers formal mirror references over profile copy when bot ); }); +test("dashboard home sanitizes mirror reference copy before surfacing it on the home orb", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + stateMap: Record }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.stateMap.memory_habit?.subline, "任务完成,意图=agent_loop,输入=你知道我现在在么..."); + assert.equal(data.stateMap.memory_habit?.context?.[0]?.text, "来源:近期长期记忆命中"); + assert.equal(data.stateMap.memory_habit?.context?.[1]?.text, "这条长期记忆再次命中了当前协作。"); + }, + { + getDashboardModule: async (params: unknown) => ({ + highlights: [], + module: (params as { module?: string }).module ?? "unknown", + summary: {}, + tab: "overview", + }), + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: [], + quick_actions: [], + trust_summary: { + has_restore_point: false, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [ + { + memory_id: "memory_strategy_weekly", + reason: "这条长期记忆再次命中了当前协作。", + summary: " 任务完成,意图=agent_loop,输入=你知道我现在在么�... ", + }, + ], + profile: null, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home rotates mirror summons across formal memory, profile, and history sections", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index dd36bc78f..a5c2b8ab7 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -321,6 +321,19 @@ function appendDistinctContextItem( return items; } +function normalizeDashboardHomeCopy(value: string | null | undefined) { + if (typeof value !== "string") { + return null; + } + + const normalized = value + .replace(/\uFFFD/g, "") + .replace(/\s+/g, " ") + .trim(); + + return normalized === "" ? null : normalized; +} + function dedupeSummonTemplates(templates: Array>) { const seen = new Set(); @@ -799,21 +812,29 @@ function buildReferenceMirrorState( } const state = createFormalMirrorStateBase(stateKey); - const summary = latestReference.summary || latestReference.reason || "最近有一条长期记忆再次命中当前协作。"; + const summary = + normalizeDashboardHomeCopy(latestReference.summary) + ?? normalizeDashboardHomeCopy(latestReference.reason) + ?? "最近有一条长期记忆再次命中当前协作。"; + const reason = normalizeDashboardHomeCopy(latestReference.reason); state.headline = "近期被调用记忆"; state.subline = summary; - state.context = [ - { - iconKey: "link", - text: `记忆 ID:${latestReference.memory_id}`, - type: "hint", - }, - { - iconKey: "brain", - text: `命中内容:${summary}`, - type: "active", - }, - ]; + state.context = []; + appendDistinctContextItem(state.context, { + iconKey: "link", + text: "来源:近期长期记忆命中", + type: "hint", + }); + appendDistinctContextItem( + state.context, + reason && reason !== summary + ? { + iconKey: "brain", + text: reason, + type: "active", + } + : null, + ); state.insights = undefined; state.navigationTarget = buildMirrorDetailNavigationTarget("memory", "打开镜子页", latestReference.memory_id); return state; From 98c37619a1090c70bcd6ae350d7115265d36d9bf Mon Sep 17 00:00:00 2001 From: xgopilot Date: Sun, 10 May 2026 07:18:05 +0000 Subject: [PATCH 18/19] fix(dashboard): align orb summon routing copy Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: gdemonc <146809967+gdemonc@users.noreply.github.com> --- .../src/app/dashboard/DashboardHome.tsx | 29 ++++++- .../dashboard/dashboard.contract.test.ts | 85 ++++++++++++++++++- .../dashboard/home/dashboardHome.service.ts | 5 +- 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/dashboard/DashboardHome.tsx b/apps/desktop/src/app/dashboard/DashboardHome.tsx index ee760c0f5..d2db833c9 100644 --- a/apps/desktop/src/app/dashboard/DashboardHome.tsx +++ b/apps/desktop/src/app/dashboard/DashboardHome.tsx @@ -83,6 +83,20 @@ function pickNextSummonIndex( return (nextIndex + 1) % total; } +function buildSummonTemplateSignature(templates: Array>) { + return templates + .map((template) => [ + template.stateKey, + template.module, + template.message, + template.reason, + template.nextStep ?? "", + template.priority, + template.recommendationId ?? "", + ].join("::")) + .join("||"); +} + type DashboardHomeProps = { data: DashboardHomeData; onVoiceOpen: () => void; @@ -108,6 +122,8 @@ export function DashboardHome({ const lastSummonIndexRef = useRef(-1); const lastSummonModuleRef = useRef(null); const summonTimerRef = useRef(null); + const summonTemplatesRef = useRef(data.summonTemplates); + const summonTemplateSignature = buildSummonTemplateSignature(data.summonTemplates); const activeState = activeExpandedState ?? (activeStateKey ? data.stateMap[activeStateKey] : null); const activeModule = hoveredEntranceKey @@ -124,19 +140,20 @@ export function DashboardHome({ }, []); const scheduleSummon = useCallback(() => { - if (data.summonTemplates.length === 0) { + const templates = summonTemplatesRef.current; + if (templates.length === 0) { return; } // Summons are local presentation state, so balancing the module order here // does not change the formal dashboard data contract or backend ranking. const nextIndex = pickNextSummonIndex( - data.summonTemplates, + templates, lastSummonIndexRef.current, lastSummonModuleRef.current, ); lastSummonIndexRef.current = nextIndex; - const template = data.summonTemplates[nextIndex]; + const template = templates[nextIndex]; lastSummonModuleRef.current = template.module; setSummons((current) => { @@ -155,6 +172,10 @@ export function DashboardHome({ const gap = (template.duration ?? 5_000) + 7_000; summonTimerRef.current = window.setTimeout(scheduleSummon, gap); + }, []); + + useEffect(() => { + summonTemplatesRef.current = data.summonTemplates; }, [data.summonTemplates]); useEffect(() => { @@ -174,7 +195,7 @@ export function DashboardHome({ window.clearTimeout(summonTimerRef.current); } }; - }, [data.summonTemplates.length, scheduleSummon]); + }, [data.summonTemplates.length, scheduleSummon, summonTemplateSignature]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { diff --git a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts index 654a68801..6542f51d0 100644 --- a/apps/desktop/src/features/dashboard/dashboard.contract.test.ts +++ b/apps/desktop/src/features/dashboard/dashboard.contract.test.ts @@ -1793,12 +1793,18 @@ test("dashboard home randomizes summons while preferring a different module when const dashboardHomeSource = readFileSync(resolve(desktopRoot, "src/app/dashboard/DashboardHome.tsx"), "utf8"); assert.match(dashboardHomeSource, /function pickNextSummonIndex\(/); + assert.match(dashboardHomeSource, /function buildSummonTemplateSignature\(/); assert.match(dashboardHomeSource, /if \(previousIndex < 0 \|\| previousModule === null\) \{/); assert.match(dashboardHomeSource, /return 0;/); assert.match(dashboardHomeSource, /candidate\.module !== previousModule/); assert.match(dashboardHomeSource, /const pool = candidateIndexes\.length > 0 \? candidateIndexes : fallbackIndexes/); assert.match(dashboardHomeSource, /Math\.floor\(Math\.random\(\) \* pool\.length\)/); assert.match(dashboardHomeSource, /lastSummonModuleRef\.current = template\.module/); + assert.match(dashboardHomeSource, /const summonTemplatesRef = useRef\(data\.summonTemplates\)/); + assert.match(dashboardHomeSource, /const summonTemplateSignature = buildSummonTemplateSignature\(data\.summonTemplates\)/); + assert.match(dashboardHomeSource, /const templates = summonTemplatesRef\.current/); + assert.match(dashboardHomeSource, /summonTemplatesRef\.current = data\.summonTemplates/); + assert.match(dashboardHomeSource, /\}, \[data\.summonTemplates\.length, scheduleSummon, summonTemplateSignature\]\);/); assert.match(dashboardHomeSource, /const closeActiveOverlay = useCallback\(\(\) => \{/); assert.match(dashboardHomeSource, /if \(event\.key === "Escape" && \(activeStateKey \|\| activeExpandedState\)\) \{/); assert.match(dashboardHomeSource, /onClose=\{closeActiveOverlay\}/); @@ -6681,7 +6687,7 @@ test("dashboard home prioritizes overview and module signals before recommendati assert.ok(data.summonTemplates.length >= 3); assert.deepEqual(data.summonTemplates.slice(0, 3).map((item) => item.module), ["safety", "tasks", "memory"]); - assert.equal(data.summonTemplates[0]?.message, "刚生成了新的摘要草稿。"); + assert.equal(data.summonTemplates[0]?.message, "当前有 2 项操作等待授权"); assert.equal(data.summonTemplates[1]?.nextStep, "打开任务详情"); assert.equal(data.stateMap.task_working?.navigationTarget?.kind, "task_detail"); assert.equal(data.stateMap.task_working?.navigationTarget?.taskId, "task_focus_001"); @@ -6823,6 +6829,83 @@ test("dashboard home prioritizes overview and module signals before recommendati ); }); +test("dashboard home keeps urgent safety summons aligned with safety copy instead of global task signals", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + summonTemplates: Array<{ message: string; module: string; nextStep?: string; reason: string; stateKey: string }>; + stateMap: Record; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + const safetySummon = data.summonTemplates.find((item) => item.stateKey === "safety_alert"); + assert.equal(safetySummon?.module, "safety"); + assert.equal(safetySummon?.message, "当前有 1 项操作等待授权"); + assert.equal(safetySummon?.reason, "建议先处理待授权操作,再继续推进其它任务。"); + assert.equal(safetySummon?.nextStep, "处理待授权操作"); + assert.equal(data.stateMap.safety_alert?.headline, "当前有 1 项操作等待授权"); + assert.equal(data.stateMap.safety_alert?.navigationTarget?.kind, "module"); + assert.equal(data.stateMap.safety_alert?.navigationTarget?.label, "处理待授权操作"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + return { + highlights: [], + module: moduleName, + summary: {}, + tab: moduleName === "tasks" ? "focus" : "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: { + current_step: "生成摘要", + next_action: "等待处理完成", + status: "processing", + task_id: "task_focus_001", + title: "整理 Q3 复盘要点", + updated_at: "2026-04-07T10:40:00+08:00", + }, + high_value_signal: ["刚生成了新的摘要草稿。"], + quick_actions: ["处理待授权操作", "打开任务详情"], + trust_summary: { + has_restore_point: true, + pending_authorizations: 1, + risk_level: "yellow", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: [], + memory_references: [], + profile: null, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home reuses formal mirror profile fields for memory copy", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index a5c2b8ab7..63d4dd0bc 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -1106,7 +1106,10 @@ function buildOverviewSummons( if (hasUrgentSafetySignal) { templates.push({ duration: 6_200, - message: highValueSignals[0] ?? safetyState.headline, + // Safety summons are hard-routed to the safety module. Keep their + // headline anchored to safety state so task-oriented overview copy does + // not leak into a different navigation target. + message: safetyState.headline, module: "safety", nextStep: getSummonNextStep(safetyState), priority: "urgent", From 37582e4e4af66c32e4603c496186f329513782e0 Mon Sep 17 00:00:00 2001 From: xgopilot Date: Sun, 10 May 2026 08:32:55 +0000 Subject: [PATCH 19/19] fix(dashboard): align orb fallback routing Generated with [codeagent](https://github.com/qbox/codeagent) Co-authored-by: gdemonc <146809967+gdemonc@users.noreply.github.com> --- .../src/app/dashboard/DashboardHome.tsx | 36 ++++++++++ .../dashboard/dashboard.contract.test.ts | 69 +++++++++++++++++++ .../dashboard/home/dashboardHome.service.ts | 55 ++++++++++++++- 3 files changed, 159 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/dashboard/DashboardHome.tsx b/apps/desktop/src/app/dashboard/DashboardHome.tsx index d2db833c9..1659974f2 100644 --- a/apps/desktop/src/app/dashboard/DashboardHome.tsx +++ b/apps/desktop/src/app/dashboard/DashboardHome.tsx @@ -84,6 +84,39 @@ function pickNextSummonIndex( } function buildSummonTemplateSignature(templates: Array>) { + const buildNavigationTargetSignature = ( + target: DashboardHomeSummonEvent["expandedState"]["navigationTarget"] | undefined, + ) => { + if (!target) { + return ""; + } + + if (target.kind === "task_detail") { + return [ + target.kind, + target.module, + target.label, + target.taskId, + ].join("::"); + } + + if (target.kind === "mirror_detail") { + return [ + target.kind, + target.module, + target.label, + target.activeDetailKey, + target.focusMemoryId ?? "", + ].join("::"); + } + + return [ + target.kind, + target.module, + target.label, + ].join("::"); + }; + return templates .map((template) => [ template.stateKey, @@ -93,6 +126,9 @@ function buildSummonTemplateSignature(templates: Array \{/); assert.match(dashboardHomeSource, /if \(event\.key === "Escape" && \(activeStateKey \|\| activeExpandedState\)\) \{/); @@ -7235,6 +7239,71 @@ test("dashboard home keeps task-detail CTA copy when the first quick action targ ); }); +test("dashboard home routes overview fallback signals to the inferred module instead of defaulting to tasks", async () => { + await withDesktopAliasRuntime( + async (requireFn) => { + const modulePath = resolve(desktopRoot, "src/features/dashboard/home/dashboardHome.service.ts"); + delete requireFn.cache[modulePath]; + + const service = requireFn(modulePath) as { + loadDashboardHomeData: () => Promise<{ + summonTemplates: Array<{ message: string; module: string; nextStep?: string; stateKey: string }>; + }>; + }; + + const data = await service.loadDashboardHomeData(); + + assert.equal(data.summonTemplates[0]?.message, "镜子里新增了一条历史概要"); + assert.equal(data.summonTemplates[0]?.module, "memory"); + assert.equal(data.summonTemplates[0]?.stateKey, "memory_summary"); + assert.equal(data.summonTemplates[0]?.nextStep, "打开镜子页"); + }, + { + getDashboardModule: async (params: unknown) => { + const moduleName = (params as { module?: string }).module ?? "unknown"; + return { + highlights: moduleName === "memory" ? ["本周复盘已经形成初稿"] : [], + module: moduleName, + summary: {}, + tab: "overview", + }; + }, + getDashboardOverview: async () => ({ + overview: { + focus_summary: null, + high_value_signal: ["镜子里新增了一条历史概要"], + quick_actions: ["打开任务详情"], + trust_summary: { + has_restore_point: false, + pending_authorizations: 0, + risk_level: "green", + workspace_path: "workspace", + }, + }, + }), + getRecommendations: async () => ({ + cooldown_hit: false, + items: [], + }), + getMirrorOverview: async () => ({ + daily_summary: null, + history_summary: ["这里是最近一条历史概要。", "第二条历史概要。"], + memory_references: [], + profile: null, + }), + listNotepad: async () => ({ + items: [], + page: { + has_more: false, + limit: 12, + offset: 0, + total: 0, + }, + }), + }, + ); +}); + test("dashboard home prefers formal mirror references over profile copy when both exist", async () => { await withDesktopAliasRuntime( async (requireFn) => { diff --git a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts index 63d4dd0bc..0d2a102fb 100644 --- a/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts +++ b/apps/desktop/src/features/dashboard/home/dashboardHome.service.ts @@ -283,6 +283,59 @@ function getOverviewQuickActions(overview: AgentDashboardOverviewGetResult) { : []; } +function inferModuleFromOverviewSignal(signal: string): DashboardHomeModuleKey { + const corpus = signal.toLowerCase(); + + if ( + corpus.includes("memory") || + corpus.includes("mirror") || + corpus.includes("profile") || + corpus.includes("history") || + corpus.includes("summary") || + signal.includes("镜") || + signal.includes("记忆") || + signal.includes("画像") || + signal.includes("历史") || + signal.includes("总结") + ) { + return "memory"; + } + + if ( + corpus.includes("safety") || + corpus.includes("security") || + corpus.includes("approval") || + corpus.includes("authorize") || + corpus.includes("audit") || + corpus.includes("recovery") || + corpus.includes("risk") || + signal.includes("授权") || + signal.includes("安全") || + signal.includes("恢复") || + signal.includes("审计") || + signal.includes("风险") + ) { + return "safety"; + } + + if ( + corpus.includes("todo") || + corpus.includes("note") || + corpus.includes("notes") || + corpus.includes("reminder") || + corpus.includes("schedule") || + corpus.includes("recurring") || + signal.includes("便签") || + signal.includes("提醒") || + signal.includes("日程") || + signal.includes("待办") + ) { + return "notes"; + } + + return "tasks"; +} + function matchesNavigationActionLabel(state: DashboardHomeStateData, actionLabel: string) { return state.navigationTarget?.label.trim() === actionLabel.trim(); } @@ -1146,7 +1199,7 @@ function buildOverviewSummons( if (templates.length === 0 && highValueSignals[0]) { const targetState = trustSummary.pending_authorizations > 0 || trustSummary.risk_level !== "green" ? safetyState - : taskState; + : stateMap[stateKeys[inferModuleFromOverviewSignal(highValueSignals[0])]]; templates.push({ duration: 5_800,