diff --git a/apps/presentation/dashboard/smoke/task-board-scroll-smoke.mjs b/apps/presentation/dashboard/smoke/task-board-scroll-smoke.mjs index 77d5182023..fa8fde90f1 100644 --- a/apps/presentation/dashboard/smoke/task-board-scroll-smoke.mjs +++ b/apps/presentation/dashboard/smoke/task-board-scroll-smoke.mjs @@ -32,7 +32,7 @@ assert.match( ); assert.match( styles, - /\.personal-channel-scroll:has\(\.personal-task-board\) \{ overflow: hidden; \}/, + /\.personal-channel-scroll\[data-active-goal-view="tasks"\]:has\(\.personal-task-board\) \{ overflow: hidden; \}/, "Desktop Tasks keeps the page chrome fixed while lanes scroll", ); assert.match( @@ -52,7 +52,7 @@ assert.match( ); assert.match( styles, - /@media \(max-width: 720px\)[\s\S]*\.personal-channel-scroll:has\(\.personal-task-board\) \{ overflow-y: auto; \}[\s\S]*\.personal-task-lane-scroll,[^}]*\{[^}]*overflow: visible;/, + /@media \(max-width: 720px\)[\s\S]*\.personal-channel-scroll\[data-active-goal-view="tasks"\]:has\(\.personal-task-board\) \{ overflow-y: auto; \}[\s\S]*\.personal-task-lane-scroll,[^}]*\{[^}]*overflow: visible;/, "Narrow screens fall back to one page scroller instead of nested scroll traps", ); assert.match( @@ -137,12 +137,13 @@ async function main() { const url = `http://127.0.0.1:${port}/?statusUrl=/status.json`; await waitForHttp(url); browser = await chromium.launch({ headless: true }); - const page = await browser.newPage({ viewport: { width: 1512, height: 760 } }); + const page = await browser.newPage({ locale: "zh-CN", viewport: { width: 1512, height: 760 } }); const fixture = statusFixture(); await page.route(`http://127.0.0.1:${port}/status.json**`, (route) => route.fulfill({ contentType: "application/json", json: fixture, status: 200 })); await page.route("**/api/**", (route) => route.fulfill({ contentType: "application/json", json: { ok: true }, status: 200 })); await page.goto(url, { waitUntil: "networkidle" }); await page.locator(".personal-goal-link").filter({ hasText: "Task board scroll fixture" }).click(); + await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: /^(Tasks|任务)$/ }).click(); const lane = page.getByRole("region", { name: "待执行 / 进行中" }); await lane.waitFor({ state: "visible" }); diff --git a/apps/presentation/dashboard/smoke/task-inspector-route-smoke.mjs b/apps/presentation/dashboard/smoke/task-inspector-route-smoke.mjs index 550cfd1422..9e20ad4457 100644 --- a/apps/presentation/dashboard/smoke/task-inspector-route-smoke.mjs +++ b/apps/presentation/dashboard/smoke/task-inspector-route-smoke.mjs @@ -22,6 +22,21 @@ try { const menu = page.locator(".personal-mobile-menu"); if (await menu.isVisible()) await menu.click(); await page.locator(".personal-goal-link", { hasText: "Progress Projection" }).click(); + const activeTab = page.locator('.personal-goal-tabs button[aria-current="page"]'); + await activeTab.filter({ hasText: "概览" }).waitFor(); + const goalLink = new URL(page.url()); + goalLink.searchParams.delete("view"); + await page.goto(goalLink.href, { waitUntil: "networkidle" }); + await activeTab.filter({ hasText: "概览" }).waitFor(); + await page.reload({ waitUntil: "networkidle" }); + await activeTab.filter({ hasText: "概览" }).waitFor(); + await page.screenshot({ path: resolve(outputDir, `goal-default-overview-${width}.png`), animations: "disabled" }); + for (const [view, label] of [["conversation", "对话"], ["files", "成果"], ["tasks", "任务"]]) { + const explicitLink = new URL(goalLink); + explicitLink.searchParams.set("view", view); + await page.goto(explicitLink.href, { waitUntil: "networkidle" }); + await activeTab.filter({ hasText: label }).waitFor(); + } const opener = page.locator(".personal-task-card", { hasText: "Current Todo" }).locator(":scope > button"); const drawer = page.getByRole("dialog", { name: "Todo 详情" }); const scope = () => { @@ -86,7 +101,7 @@ try { await workspace.close(); } } - console.log("task-inspector-route: passed (packaged desktop/390px, keyboard/reduced motion, close/reload, Back/Forward, deliberate and unavailable deep links, no model or authority writes)"); + console.log("task-inspector-route: passed (packaged desktop/390px, default overview and explicit view links, keyboard/reduced motion, close/reload, Back/Forward, deliberate and unavailable deep links, no model or authority writes)"); } finally { await browser?.close(); server?.kill("SIGTERM"); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx index d982251735..0f9079ad76 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx @@ -924,7 +924,7 @@ export function PersonalWorkspacePage({ const [taskInspectorExpanded, setTaskInspectorExpanded] = useState(false); const [activeSessionRun, setActiveSessionRun] = useState(null); const [proposals, setProposals] = useState>({}); - const [localView, setLocalView] = useState(controlledGoalId ? "chat" : "overview"); + const [localView, setLocalView] = useState("overview"); const selectedGoalTab = controlledView ?? localView; const agentWorkActive = Boolean(controlledGoalId === undefined ? localGoalId : controlledGoalId) && selectedGoalTab === "chat" && selectedWorkAgentId != null; const workReturn = useRef<{scope: string; agentId: string} | null>(null); @@ -1904,7 +1904,7 @@ export function PersonalWorkspacePage({ onOpenOutput: drawerCallbacks.onOpenOutput, } : drawerCallbacks; - function selectGoal(goalId: string | null, view: WorkspaceGoalTab = goalId ? "tasks" : "overview") { + function selectGoal(goalId: string | null, view: WorkspaceGoalTab = "overview") { setLocalGoalId(goalId); setManagerConversationReceiptVisible(false); setGoalConversationReceiptVisible(false); diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx index 5877abf0dd..4d6de507d8 100644 --- a/apps/presentation/dashboard/src/views/dashboard-page.tsx +++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx @@ -3509,7 +3509,7 @@ export function DashboardPage() { void navigate({search: current => "goalId" in current && current.goalId === goalId && current.statusUrl === statusUrl ? {...current, todo: todoId ?? undefined} : current}); }} - selectedView={search.view === "conversation" ? "chat" : search.goalId ? search.view ?? "chat" : "overview"} + selectedView={search.view === "conversation" ? "chat" : search.goalId ? search.view ?? "overview" : "overview"} onSelectView={(view) => { void navigate({ search: current => ({ ...current, workAgent: undefined, view: view === "chat" ? "conversation" : view }) }); }} diff --git a/docs/architecture/rfcs/goal-workspace-surface-v0.md b/docs/architecture/rfcs/goal-workspace-surface-v0.md index 6c1fb4b42b..eb434cda4e 100644 --- a/docs/architecture/rfcs/goal-workspace-surface-v0.md +++ b/docs/architecture/rfcs/goal-workspace-surface-v0.md @@ -184,6 +184,9 @@ screen.* | 8 | **All-work rail**: tabs All work / Activity, filter chips, sections | ≥1200px; drawer below | Complete inventory without leaving the brief | | 9 | Existing **Delivery and evidence** section | As today | Stage 4 contract unchanged, below Results | +Opening a Goal from the sidebar, workspace home or a Goal URL without a view +starts on Overview. Explicit Tasks, Chat and Results links keep their destination. + Chat remains a direct destination. "Discuss" on a decision or row opens Goal Chat with that subject attached through the existing composer; it does not create a second conversation store. diff --git a/examples/personal-workspace-browser/navigation-sorting.mjs b/examples/personal-workspace-browser/navigation-sorting.mjs index fde6ef25d6..9e31dcea87 100644 --- a/examples/personal-workspace-browser/navigation-sorting.mjs +++ b/examples/personal-workspace-browser/navigation-sorting.mjs @@ -159,6 +159,7 @@ export const navigationSortingScenario = { await page.getByRole("button", { name: /关闭详情/ }).click(); pass(26, "Overview keeps direct decisions, explicit unknown usage and read failures; output provenance is one click away."); await page.locator(".personal-goal-link").filter({ hasText: "Progress Projection" }).click(); + await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: /^(Tasks|任务)$/ }).click(); await page.getByRole("button", { name: /^(Tasks|任务)$/, current: "page" }).waitFor({ state: "visible" }); await page.locator(".personal-task-card", { hasText: "Deferred queue task" }).locator("button").first().click(); const dateResumeDrawer = page.getByRole("dialog", { name: "Todo 详情" }); @@ -179,13 +180,13 @@ export const navigationSortingScenario = { pass(22, "Todo detail renders the shared typed date condition, ready state, and stable receipt id."); // A visit to another view or settings must not discard loaded task history. - const historyScroll = page.getByTestId("completed-task-lane").getByRole("region"); + const historyScroll = page.locator('[data-goal-panel="tasks"]').getByTestId("completed-task-lane").getByRole("region"); await page.waitForFunction(() => { - const lane = document.querySelector('[data-testid="completed-task-lane"] .personal-task-lane-scroll'); + const lane = document.querySelector('[data-goal-panel="tasks"] [data-testid="completed-task-lane"] .personal-task-lane-scroll'); return lane && lane.scrollHeight > lane.clientHeight + 740; }); await historyScroll.evaluate(element => { element.scrollTop = 740; }); - await page.waitForFunction(() => document.querySelector('[data-testid="completed-task-lane"] .personal-task-lane-scroll').scrollTop === 740); + await page.waitForFunction(() => document.querySelector('[data-goal-panel="tasks"] [data-testid="completed-task-lane"] .personal-task-lane-scroll').scrollTop === 740); const retainedHistory = await historyScroll.elementHandle(); const goalViews = page.getByRole("navigation", { name: "Goal 视图" }); for (const name of ["概览", "成果", "对话", "任务"]) { @@ -203,6 +204,7 @@ export const navigationSortingScenario = { throw new Error("Settings return reset loaded task history"); } await page.locator(".personal-goal-link", { hasText: "Multi Agent Projection" }).click(); + await goalViews.getByRole("button", { name: "任务", exact: true }).click(); const laneFilter = page.getByRole("combobox", { name: "按工作 Agent 筛选", exact: true }); await laneFilter.selectOption("codex-older-lane"); await goalViews.getByRole("button", { name: "概览", exact: true }).click(); diff --git a/examples/personal-workspace-browser/showcase-workspace.mjs b/examples/personal-workspace-browser/showcase-workspace.mjs index 47f64a0866..803065dc6e 100644 --- a/examples/personal-workspace-browser/showcase-workspace.mjs +++ b/examples/personal-workspace-browser/showcase-workspace.mjs @@ -155,6 +155,6 @@ export async function installShowcaseWorkspace(api, page, state) { export async function openShowcaseGoal(page) { await page.locator(".personal-goal-row").filter({ has: page.locator("strong", { hasText: title }) }).first().locator(".personal-goal-link").click(); await page.getByTestId("goal-vitals").waitFor({ state: "visible", timeout: 15_000 }); - await page.getByRole("navigation", { name: /^(Goal 视图|Goal view)$/ }).getByRole("button", { name: /^(概览|Overview)$/ }).click(); + await page.getByRole("navigation", { name: /^(Goal 视图|Goal view)$/ }).getByRole("button", { name: /^(概览|Overview)$/, current: "page" }).waitFor(); await page.locator(".goal-now, .work-map, .delivery-review [role=alert]").first().waitFor({ timeout: 15_000 }); } diff --git a/examples/personal-workspace-browser/task-inspector-return.mjs b/examples/personal-workspace-browser/task-inspector-return.mjs index df9952bf0e..897327be36 100644 --- a/examples/personal-workspace-browser/task-inspector-return.mjs +++ b/examples/personal-workspace-browser/task-inspector-return.mjs @@ -23,6 +23,7 @@ export const taskInspectorReturnScenario = { const menu = page.locator(".personal-mobile-menu"); if (await menu.isVisible()) await menu.click(); await page.locator(".personal-goal-link", { hasText: "Progress Projection" }).click(); + await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: /^(Tasks|任务)$/ }).click(); const card = page.locator(".personal-task-card", { hasText: "Current Todo" }); const opener = card.locator(":scope > button"); await opener.waitFor(); diff --git a/examples/personal-workspace-browser/typed-actions.mjs b/examples/personal-workspace-browser/typed-actions.mjs index c964701c68..6eb4081b21 100644 --- a/examples/personal-workspace-browser/typed-actions.mjs +++ b/examples/personal-workspace-browser/typed-actions.mjs @@ -841,8 +841,9 @@ export const typedActionsScenario = { const goalButton = page.locator(".personal-goal-link").first(); await goalButton.click(); const goalNavigation = page.getByRole("navigation", { name: "Goal 视图" }); - const defaultTasksTab = goalNavigation.getByRole("button", { name: /^(Tasks|任务)$/ }); - if (await defaultTasksTab.getAttribute("aria-current") !== "page") throw new Error("Selecting a Goal did not prioritize its Tasks view"); + await goalNavigation.getByRole("button", { name: /^(Overview|概览)$/, current: "page" }).waitFor(); + const tasksTab = goalNavigation.getByRole("button", { name: /^(Tasks|任务)$/ }); + await tasksTab.click(); await page.screenshot({ path: resolve(outputDir, "goal-tasks-loopx-theme.png"), fullPage: false, animations: "disabled" }); await goalNavigation.getByRole("button", { name: /^(Files|成果)$/ }).click(); const publicFiles = page.locator(".personal-files-list > button"); @@ -852,7 +853,7 @@ export const typedActionsScenario = { await goalNavigation.getByRole("button", { name: /^(Chat|对话)$/ }).click(); await page.locator(".personal-channel-timeline").waitFor({ state: "visible" }); await page.screenshot({ path: resolve(outputDir, "goal-chat-loopx-theme.png"), fullPage: false, animations: "disabled" }); - await defaultTasksTab.click(); + await tasksTab.click(); const desktopNavigationTrigger = page.locator(".personal-mobile-menu"); if (await desktopNavigationTrigger.isVisible()) { throw new Error("Mobile-only Goal navigation trigger leaked into the persistent desktop sidebar layout"); @@ -891,6 +892,7 @@ export const typedActionsScenario = { await page.screenshot({ path: resolve(outputDir, "goal-header-compact-width.png"), fullPage: false, animations: "disabled" }); if (fullDesktopViewport) await page.setViewportSize(fullDesktopViewport); await page.locator(".personal-goal-link", { hasText: "Progress Projection" }).click(); + await tasksTab.click(); await page.getByRole("heading", { name: "Progress Projection" }).waitFor({ state: "visible" }); await page.locator(".personal-task-card").getByText("Current Todo", {exact: true}).waitFor(); const unclaimedCard = page.locator(".personal-task-card", {hasText: "Current Todo"}); @@ -948,7 +950,7 @@ export const typedActionsScenario = { await historyScroll.evaluate(element => { element.scrollTop = element.scrollHeight; }); await response; await page.waitForFunction(minimum => { - const window = document.querySelector('[data-testid="completed-task-lane"] .personal-completed-window'); + const window = document.querySelector('[data-goal-panel="tasks"] [data-testid="completed-task-lane"] .personal-completed-window'); return window && Number.parseFloat(window.style.height) >= minimum; }, Math.min(4087, (batch + 1) * 40) * 148); if (await completedColumn.locator('.personal-completed-row').count() > 20) throw new Error('Completed history DOM grew with accumulated pages'); @@ -975,7 +977,7 @@ export const typedActionsScenario = { page.on('request', request => { if (request.url().includes('/api/chat/completed-todos?')) historyRequests += 1; }); await page.getByRole('button', { name: '列表', exact: true }).click(); await assertDeferredTask(); - const listHistory = page.getByTestId('completed-task-lane'); + const listHistory = page.locator('[data-goal-panel="tasks"]').getByTestId('completed-task-lane'); await listHistory.getByRole('button', { name: '已完成', exact: false }).click(); await listHistory.getByText('4087', { exact: true }).waitFor(); await listHistory.getByText('Completed A', { exact: true }).waitFor(); @@ -987,6 +989,7 @@ export const typedActionsScenario = { await completedColumn.getByText('Completed A', { exact: true }).waitFor(); if (historyRequests) throw new Error('Switching presentation replaced the completed-history snapshot'); await page.locator(".personal-goal-link", { hasText: "Multi Agent Projection" }).click(); + await tasksTab.click(); const multiAgentHeader = await page.locator(".personal-channel-title").innerText(); if (multiAgentHeader.includes("codex-older-lane")) { throw new Error(`Multi-Agent Goal header still implies arbitrary single-lane ownership: ${multiAgentHeader}`); @@ -1005,6 +1008,7 @@ export const typedActionsScenario = { const runtimeSelector = page.getByRole("combobox", { name: "选择聊天 Runtime" }); if (!await runtimeSelector.count()) throw new Error("Chat runtime selector is not explicitly labelled independently from work-Agent lanes"); await goalButton.click(); + await tasksTab.click(); const readBoardGeometry = async () => { const kanban = page.locator(".personal-task-kanban"); await kanban.waitFor({ state: "visible" }); @@ -1028,6 +1032,7 @@ export const typedActionsScenario = { }; const selectFirstGoal = async () => { await page.locator(".personal-goal-link").first().click(); + await tasksTab.click(); await page.locator(".personal-task-kanban").waitFor({ state: "visible" }); }; const selectProductReleaseGoal = async () => { @@ -1037,6 +1042,7 @@ export const typedActionsScenario = { if (await stoppedGoals.getAttribute("open") === null) await stoppedGoals.locator("summary").click(); } await goal.click(); + await tasksTab.click(); await page.locator(".personal-task-kanban").waitFor({ state: "visible" }); }; const populatedGeometry = await readBoardGeometry(); @@ -1062,7 +1068,7 @@ export const typedActionsScenario = { await page.setViewportSize(desktopViewport); await page.waitForTimeout(200); await selectFirstGoal(); - await page.locator(".personal-object-list").first().waitFor({ state: "visible" }); + await page.locator('[data-goal-panel="tasks"] .personal-object-list').first().waitFor({ state: "visible" }); if (await page.locator(".personal-task-capability-callout").count()) throw new Error("Goal capability settings still consume a full-width Tasks row"); await page.getByRole("button", { name: "Goal 设置", exact: true }).click(); await page.getByRole("heading", { level: 1, name: "能力中心", exact: true }).waitFor({ state: "visible" }); @@ -1602,7 +1608,7 @@ export const typedActionsScenario = { await page.getByRole("button", { name: "返回工作区", exact: true }).click(); await selectProductReleaseGoal(); await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: /^(Tasks|任务)$/ }).click(); - await page.locator(".personal-object-list").first().waitFor({ state: "visible" }); + await page.locator('[data-goal-panel="tasks"] .personal-object-list').first().waitFor({ state: "visible" }); await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: /^(Files|成果)$/ }).click(); const reportOutput = page.getByTestId("personal-goal-outputs").getByRole("button", { name: /Product Release milestone report/ }); await reportOutput.waitFor({ state: "visible" }); @@ -1986,7 +1992,7 @@ export const typedActionsScenario = { api.nextStatusDelayMs = 1_600; await page.getByRole("button", { name: "查看更新后的 Goal", exact: true }).click(); await page.getByRole("dialog").filter({ hasText: "执行结果" }).waitFor({ state: "hidden", timeout: 600 }); - await goalNavigation.getByRole("button", { name: /^(Tasks|任务)$/, current: "page" }).waitFor({ state: "visible", timeout: 600 }); + await goalNavigation.getByRole("button", { name: /^(Overview|概览)$/, current: "page" }).waitFor({ state: "visible", timeout: 600 }); await goalNavigation.getByRole("button", { name: /^(Chat|对话)$/ }).click(); } else { await page.getByRole("button", { name: "关闭", exact: true }).click(); @@ -2066,7 +2072,7 @@ export const typedActionsScenario = { await needsYouCard.click(); await page.getByRole("heading", { name: needsYouSource }).waitFor({ state: "visible" }).catch(() => {}); await page.getByText(needsYouAction, { exact: true }).first().waitFor({ state: "visible" }); - await page.locator(".personal-object-list").first().getByRole("button").first().click(); + await page.locator(".goal-overview-attention button", { hasText: needsYouAction }).first().click(); await page.getByText("需要你", { exact: true }).last().waitFor({ state: "visible" }); await page.getByText("更多决定").click(); const writesBeforeDecision = api.durableWriteCount; diff --git a/examples/personal-workspace-browser/workspace-view-recovery.mjs b/examples/personal-workspace-browser/workspace-view-recovery.mjs index 8e2a1e9fd4..ae6cbd3959 100644 --- a/examples/personal-workspace-browser/workspace-view-recovery.mjs +++ b/examples/personal-workspace-browser/workspace-view-recovery.mjs @@ -34,7 +34,7 @@ export const workspaceViewRecoveryScenario = { assert.equal(await active(managerTabs, "总览"), "page", "Forward restores the overview"); await page.locator(".personal-goal-link").filter({ hasText: "Product Release" }).click(); - assert.equal(await active(goalTabs, "任务"), "page", "Selecting a Goal keeps the existing task-first entry"); + assert.equal(await active(goalTabs, "概览"), "page", "Selecting a Goal opens its overview"); await goalTabs.getByRole("button", { name: "成果", exact: true }).click(); await page.reload({ waitUntil: "networkidle" }); assert.equal(await active(goalTabs, "成果"), "page", "The same route owner restores Goal views");