diff --git a/apps/presentation/dashboard/smoke/goal-workspace-craft-browser-smoke.mjs b/apps/presentation/dashboard/smoke/goal-workspace-craft-browser-smoke.mjs index ca199f23b3..bdacc81b26 100644 --- a/apps/presentation/dashboard/smoke/goal-workspace-craft-browser-smoke.mjs +++ b/apps/presentation/dashboard/smoke/goal-workspace-craft-browser-smoke.mjs @@ -33,6 +33,15 @@ try { api.transformStatus = fixture => { transform(fixture); fixture.run_history.goals.find(goal => goal.id === showcaseGoalId).display_name = longTitle; + if (state.quiet) { + const queue = fixture.attention_queue.items.find(item => item.goal_id === showcaseGoalId); + Object.assign(queue.agent_todos, { advancement_done_count: 0, done_count: 0, deferred_count: 0, open_count: 0, total_count: 0, items: [], deferred_items: [] }); + Object.assign(queue.project_asset.agent_todos, { advancement_done_count: 0, done: 0, deferred_count: 0, open: 0, total: 0, items: [], recent_completed_advancement_items: [] }); + Object.assign(queue.user_todos, { open_count: 0, total_count: 0, items: [] }); + for (const agent of fixture.agent_management_projection.agents) { + if (agent.current_todo?.goal_id === showcaseGoalId) agent.current_todo = null; + } + } }; await page.route('**/api/chat/delivery-review?*', route => route.fulfill({ json: { ok: true, goal_id: showcaseGoalId, observed_at: '2026-09-01T10:00:00Z', acceptance: null, graph: null, @@ -44,6 +53,46 @@ try { const { page } = session; try { await openShowcaseGoal(page); + const inlineRail = page.getByTestId('goal-workspace-inline-rail'); + const inventory = inlineRail.locator('.goal-workspace-inventory'); + const decision = page.locator('.goal-overview [data-attention-id="showcase-gate-window"]:visible'); + assert.equal(await decision.count(), 1, 'The owner decision is shown once in the composed Overview'); + assert.equal(await inlineRail.locator('.goal-all-work').count(), 0, 'The full inventory starts collapsed instead of repeating current work'); + assert.ok(await decision.locator('.goal-attention-text').evaluate(element => parseFloat(getComputedStyle(element).fontSize) >= 16), 'Owner decisions use readable lead-size text'); + assert.ok(await decision.locator('svg').first().evaluate(element => { + const swatch = document.createElement('span'); + swatch.style.color = 'var(--pw-caution)'; + element.parentElement.append(swatch); + const matches = getComputedStyle(element).color === getComputedStyle(swatch).color; + swatch.remove(); + return matches; + }), 'The owner-decision hand retains the theme gold'); + await inventory.locator('summary').focus(); + await page.keyboard.press('Enter'); + await inlineRail.locator('.goal-work-filters').waitFor({ state: 'visible' }); + assert.equal(await page.locator('.goal-overview [data-attention-id="showcase-gate-window"]:visible').count(), 1, 'Expanding All work does not duplicate the owner decision'); + assert.equal(await inlineRail.getByRole('heading', { name: '等待决定', exact: true }).count(), 0, 'Empty work sections do not leave duplicate-looking headings'); + assert.ok(await inlineRail.locator('.goal-work-row strong').first().evaluate(element => parseFloat(getComputedStyle(element).fontSize) >= 14), 'Inventory tasks use body-size text rather than captions'); + await inlineRail.getByRole('button', { name: '活动', exact: true }).click(); + await inlineRail.locator('.goal-material-activity').waitFor({ state: 'visible' }); + await inlineRail.getByRole('button', { name: '全部工作', exact: true }).click(); + let historyReads = 0; + page.on('request', request => { if (new URL(request.url()).pathname === '/api/chat/completed-todos') historyReads++; }); + const historyResponse = page.waitForResponse(response => new URL(response.url()).pathname === '/api/chat/completed-todos'); + await inlineRail.locator('[data-testid="completed-task-lane"] > header button').click(); + await historyResponse; + const overview = page.locator('.goal-overview'); + await overview.evaluate(element => element.style.maxWidth = '800px'); + await page.waitForFunction(() => !document.querySelector('.goal-workspace-rail .goal-all-work')); + assert.equal(await inventory.isVisible(), false, 'Container reflow hides the inline inventory'); + const readsBeforeHiddenRefresh = historyReads; + state.done++; + await page.getByRole('button', { name: '刷新状态', exact: true }).click(); + await page.waitForFunction(() => document.querySelectorAll('[data-testid="goal-vitals"] .goal-vital')[1]?.querySelector('.goal-vital-value')?.textContent === '24/29'); + assert.equal(historyReads, readsBeforeHiddenRefresh, 'Refreshing a hidden inventory does not read completed history'); + await overview.evaluate(element => element.style.removeProperty('max-width')); + await inlineRail.locator('.goal-work-filters').waitFor({ state: 'visible' }); + await inventory.locator('summary').click(); await page.getByRole("region", {name:"当前工作",exact:true}).getByRole("button",{name:"关系图",exact:true}).click(); const heading = page.locator('.personal-channel-title h1'); assert.equal(await heading.innerText(), longTitle, 'The original Goal title remains complete'); @@ -76,10 +125,25 @@ try { assert.ok(await target.evaluate(element => element === document.activeElement), 'Closing details returns focus to the task'); await page.keyboard.press('Enter'); await page.keyboard.press('Escape'); assert.equal(await map.locator('.work-map-node[aria-pressed="true"]').count(), 0, 'Escape closes the map selection'); + await inventory.locator('summary').click(); + await inlineRail.locator('.goal-all-work').waitFor({ state: 'visible' }); await page.setViewportSize({ width: 1024, height: 768 }); + await page.waitForFunction(() => !document.querySelector('.goal-workspace-rail .goal-all-work')); + assert.equal(await inventory.isVisible(), false, 'Narrow view falls back to the All work drawer instead of crowding the brief'); + assert.equal(await page.locator('.goal-overview [data-attention-id="showcase-gate-window"]:visible').count(), 1, 'The owner decision remains visible once on the narrow page'); + const openAllWork = page.getByRole('button', { name: '打开全部工作' }); + await openAllWork.click(); + const railDialog = page.locator('.goal-work-rail-dialog'); + assert.ok(await railDialog.locator('.goal-all-work.is-dialog').isVisible(), 'The narrow drawer keeps the same All work inventory reachable'); + await railDialog.locator('[data-attention-id="showcase-gate-window"]').waitFor({ state: 'visible' }); + await page.keyboard.press('Escape'); + assert.equal(await railDialog.isVisible(), false, 'Escape closes the inventory drawer'); + assert.ok(await openAllWork.evaluate(element => element === document.activeElement), 'Closing the inventory restores focus to the original control'); const measured = await heading.evaluate(element => ({ height: element.clientHeight, line: parseFloat(getComputedStyle(element).lineHeight), overflow: element.scrollWidth - element.clientWidth })); assert.ok(measured.height > measured.line && measured.overflow <= 1, 'A long title wraps without clipping or page overflow'); await page.setViewportSize({ width: 390, height: 844 }); + assert.equal(await page.locator('.goal-overview [data-attention-id="showcase-gate-window"]:visible').count(), 1, 'Phone reflow retains one owner decision'); + assert.ok(await page.locator('.goal-overview-attention .goal-attention-text').first().evaluate(element => parseFloat(getComputedStyle(element).fontSize) >= 16), 'Phone reflow does not shrink the requested reading text'); const headerHeight = await page.locator('.personal-channel-header').evaluate(element => element.getBoundingClientRect().height); assert.ok(headerHeight <= 220, 'A long Goal title cannot consume the phone viewport'); assert.equal(await heading.evaluate(element => getComputedStyle(element).overflow), 'hidden', 'Phone title clipping must survive the packaged CSS cascade, not just constrain the box height'); @@ -99,7 +163,16 @@ try { await map.getByRole('button', { name: '关闭事项详情' }).click(); assert.ok(await mobile.evaluate(element => element === document.activeElement)); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), 'The phone has no page-wide horizontal overflow'); + state.quiet = true; + await page.setViewportSize({ width: 1440, height: 1000 }); + await page.getByRole('button', { name: '刷新状态', exact: true }).click(); + await page.waitForFunction(() => document.querySelectorAll('[data-testid="goal-vitals"] .goal-vital')[1]?.querySelector('.goal-vital-value')?.textContent === '0/0'); + await inlineRail.waitFor({ state: 'detached' }); + assert.ok(await page.locator('.goal-workspace-composition').evaluate(element => { + const frame = element.getBoundingClientRect(), document = element.querySelector('.goal-workspace-document').getBoundingClientRect(); + return Math.abs(frame.width - document.width) <= 1; + }), 'A known-quiet Goal gives its reading column the full width instead of reserving a blank rail'); assert.deepEqual(session.errors, [], 'Packaged composition emits no browser errors'); - console.log('Goal workspace craft: packaged long-title, readable geometry, stationary desktop selection, close/Escape/focus, 390px and reduced-motion checks passed'); + console.log('Goal workspace craft: single gold attention rail, readable inventory, hidden-history isolation, quiet layout, long-title, stationary selection, close/Escape/focus, 390px and reduced-motion checks passed'); } finally { await session.close(); } } finally { await browser?.close(); server.kill('SIGTERM'); } diff --git a/apps/presentation/dashboard/smoke/workspace-command-palette-browser-smoke.mjs b/apps/presentation/dashboard/smoke/workspace-command-palette-browser-smoke.mjs index 1648e9c9d7..220874cd9a 100644 --- a/apps/presentation/dashboard/smoke/workspace-command-palette-browser-smoke.mjs +++ b/apps/presentation/dashboard/smoke/workspace-command-palette-browser-smoke.mjs @@ -76,6 +76,7 @@ try { await dialogFor(page).getByRole('combobox').fill('map legacy'); await page.keyboard.press('Enter'); await dialogFor(page).waitFor({state: 'hidden'}); + await page.waitForURL(/todo=showcase-map-columns/); await page.locator('[data-context-drawer]').waitFor({state: 'visible'}); assert.match(page.url(), /todo=showcase-map-columns/); // The palette on top of an inspector must not close that inspector on Escape. diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css index 1ea48763c5..5dd0b5d6e1 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css @@ -11,14 +11,13 @@ .goal-overview button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 3px; } .goal-overview button svg { flex: none; } -.goal-overview-summary { display: grid; gap: 32px; align-items: start; } -.goal-overview-summary.has-attention { grid-template-columns: minmax(0, 1fr) minmax(260px, 320px); } -.goal-overview-summary > section { display: grid; align-content: start; gap: 12px; min-width: 0; } -.goal-overview-summary strong, .goal-overview-summary p, .goal-overview-summary button span { overflow-wrap: anywhere; } -.goal-overview-summary ul { list-style: none; margin: 0; padding: 0; } +.goal-overview-direction, .goal-overview-attention { display: grid; align-content: start; gap: 12px; min-width: 0; } +.goal-overview-direction strong, .goal-overview-direction p, .goal-overview-attention button span { overflow-wrap: anywhere; } +.goal-overview-attention ul { list-style: none; margin: 0; padding: 0; } .goal-overview-heading, .goal-overview-attention header { display: flex; align-items: center; gap: 8px; min-height: 32px; } -.goal-overview-heading h2, .goal-overview-attention h2 { color: var(--pw-muted); font-size: var(--text-caption); font-weight: 500; line-height: var(--leading-caption); } +.goal-overview-heading h2 { color: var(--pw-muted); font-size: var(--text-caption); font-weight: 500; line-height: var(--leading-caption); } +.goal-overview-attention h2 { color: var(--pw-text); font-size: var(--text-body); font-weight: 500; line-height: var(--leading-body); } .goal-overview-heading > span { color: var(--pw-faint); font-size: var(--text-caption); line-height: var(--leading-caption); } .goal-overview-heading > span::before { content: "·"; padding-right: 8px; } .goal-overview .goal-overview-heading button { min-height: 32px; padding: 4px 8px; border-color: transparent; background: transparent; color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); } @@ -62,7 +61,7 @@ .goal-overview-criteria .is-accepted > span { color: var(--pw-muted); } .goal-overview-criteria .is-failed svg, .goal-overview-criteria .is-failed small { color: var(--pw-red); } -.goal-overview-summary .goal-overview-run { display: inline-flex; justify-self: start; gap: 8px; margin: 0; padding: 0; min-height: 28px; border: 0; background: transparent; } +.goal-overview-direction .goal-overview-run { display: inline-flex; justify-self: start; gap: 8px; margin: 0; padding: 0; min-height: 28px; border: 0; background: transparent; } .goal-overview-run small { color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-caption); } .goal-overview-run svg { grid-column: 2; grid-row: 1; color: var(--pw-faint); transition: transform var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out); } .goal-overview-run:hover svg { color: var(--pw-text); transform: translateX(2px); } @@ -75,11 +74,11 @@ .goal-overview-attention header small { color: var(--pw-faint); } .goal-overview-attention li { transition: opacity var(--motion-base) var(--ease-out), transform var(--motion-base) var(--ease-out); } .goal-overview-attention li + li { border-top: 1px solid var(--pw-line); } -.goal-overview .goal-overview-attention li button { display: grid; grid-template-columns: 16px minmax(0, 1fr) 14px; align-items: start; gap: 10px; width: 100%; min-height: 48px; padding: 10px 8px; border: 0; border-radius: 6px; background: transparent; } +.goal-overview .goal-overview-attention li button { display: grid; grid-template-columns: 17px minmax(0, 1fr) 14px; align-items: start; gap: 10px; width: 100%; min-height: 48px; padding: 12px 0; border: 0; border-radius: 6px; background: transparent; } .goal-overview .goal-overview-attention li button:hover { background: var(--pw-hover); } .goal-overview-attention li svg { margin-top: 2px; } .goal-overview-attention li svg:first-child { color: var(--pw-caution); } -.goal-attention-text { display: grid; gap: 2px; min-width: 0; font-size: var(--text-body); line-height: var(--leading-body); } +.goal-attention-text { display: grid; gap: 4px; min-width: 0; font-size: var(--text-lead); line-height: var(--leading-lead); } .goal-attention-text time { color: var(--pw-faint); font-size: var(--text-caption); font-variant-numeric: tabular-nums; line-height: var(--leading-caption); } .goal-overview-attention li svg:last-child { color: var(--pw-faint); transition: transform var(--motion-fast) var(--ease-out), color var(--motion-fast) var(--ease-out); } .goal-overview-attention li button:hover svg:last-child { color: var(--pw-text); transform: translateX(2px); } @@ -115,9 +114,11 @@ .goal-overview-criteria li, .goal-overview-attention li, .goal-overview-run svg, .goal-overview-attention li svg:last-child { transition: none; } } -@media (max-width: 900px) { - .goal-overview-summary.has-attention { grid-template-columns: 1fr; } - .goal-overview-attention { padding: 16px 0 0; border-top: 1px solid var(--pw-line); border-left: 0; } +@media (max-width: 1200px) { + .goal-workspace-rail .goal-overview-attention { padding: 16px 0 0; border-top: 1px solid var(--pw-line); border-left: 0; } +} +@container (max-width: 920px) { + .goal-workspace-rail .goal-overview-attention { padding: 16px 0 0; border-top: 1px solid var(--pw-line); border-left: 0; } } @media (max-width: 640px) { diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx index 562f9975bc..f272038043 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.tsx @@ -79,8 +79,9 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD {onMarkSeen ? : null}

) : null} -
-
+ {digest.objective ?

{digest.objective}

:

{copy.objectiveUnavailable}

} {currentWork && currentRow ?
{copy.current} · {goalWorkClassLabel(currentRow.displayClass, locale === "zh-CN")}{currentWork.text} @@ -92,24 +93,21 @@ export function GoalOverview({ active, goal, items, userTodos, readOnly, onOpenD
onView("tasks")}/> - - {showAttention ?
+
} + attention={showAttention ?

{copy.attention}

{blocking ?? "—"} {optional ? {copy.optional.replace("{count}", String(optional))} : null}
-
    {attention.slice(0, 3).map(item =>
  • )}
{moreAttention ? : null}
: null} - -

{copy.remote}

: }/> {readOnly ? : diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.css index a88468709c..606d6bdbf4 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.css @@ -1,10 +1,20 @@ -.goal-workspace-composition { display: grid; grid-template-columns: minmax(0, 1fr); } +.goal-workspace-composition { display: grid; grid-template-columns: minmax(0, 1fr); gap: clamp(24px, 3vw, 36px); align-items: start; } +.goal-workspace-composition.has-rail { grid-template-columns: minmax(0, 1fr) minmax(280px, 320px); } .goal-workspace-document { display: grid; gap: 32px; min-width: 0; } +.goal-workspace-rail { position: sticky; top: 16px; display: grid; gap: 24px; max-height: calc(100dvh - 32px); min-width: 0; overflow: auto; overscroll-behavior: contain; padding-left: 20px; padding-bottom: 16px; border-left: 1px solid var(--pw-line); } +.goal-workspace-rail .goal-overview-attention { padding: 0; border: 0; } +.goal-workspace-inventory { min-width: 0; } +.goal-workspace-inventory > summary { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 8px 0; color: var(--pw-text); font-size: var(--text-body); line-height: var(--leading-body); font-weight: 500; list-style: none; cursor: pointer; } +.goal-workspace-inventory > summary::-webkit-details-marker { display: none; } +.goal-workspace-inventory > summary > svg:first-child { color: var(--pw-muted); } +.goal-workspace-inventory > summary > svg:last-child { margin-left: auto; color: var(--pw-faint); } +.goal-workspace-inventory[open] > summary > svg:last-child { transform: rotate(180deg); } +.goal-workspace-inventory > summary:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 3px; border-radius: 6px; } .goal-now, .goal-workspace-results { min-width: 0; display: grid; gap: 16px; } .goal-now h2, .goal-workspace-results > h2 { font-size: var(--text-heading); line-height: var(--leading-heading); font-weight: 600; } .goal-now-header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } .goal-work-lenses, .goal-work-filters, .goal-all-work > [role="group"] { display: flex; flex-wrap: wrap; gap: 4px; } -.goal-work-lenses button, .goal-work-filters button, .goal-all-work > [role="group"] button { border-color: transparent; background: transparent; color: var(--pw-muted); padding: 6px 8px; font-size: var(--text-caption); } +.goal-work-lenses button, .goal-work-filters button, .goal-all-work > [role="group"] button { border-color: transparent; background: transparent; color: var(--pw-muted); padding: 6px 8px; font-size: var(--text-body); line-height: var(--leading-body); } .goal-work-lenses button[aria-pressed="true"], .goal-work-filters button[aria-pressed="true"], .goal-all-work > [role="group"] button[aria-pressed="true"] { color: var(--pw-text); background: var(--pw-hover); border-color: var(--pw-line); } .goal-owner-group { margin-top: 20px; } .goal-owner-group:first-child { margin-top: 0; } @@ -31,15 +41,19 @@ .goal-inline-create textarea { width: 100%; min-height: 84px; resize: vertical; border: 1px solid var(--pw-line-strong); border-radius: 6px; color: var(--pw-text); background: var(--pw-bg); padding: 10px; font: inherit; } .goal-inline-create [role="alert"] { color: var(--pw-red); } .goal-inline-create form > button { justify-self: start; } -.goal-all-work { min-width: 0; display: grid; gap: 12px; border-left: 1px solid var(--pw-line); padding-left: 20px; } +.goal-all-work { min-width: 0; display: grid; gap: 12px; padding-top: 8px; } .goal-work-search { display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--pw-line); color: var(--pw-muted); } .goal-work-search input { width: 100%; min-width: 0; border: 0; padding: 10px 0; background: transparent; color: var(--pw-text); font: inherit; font-size: var(--text-body); } .goal-rail-section { display: grid; gap: 4px; } -.goal-rail-section h3 { padding-top: 8px; color: var(--pw-muted); font-size: var(--text-caption); font-weight: 500; } +.goal-rail-section h3 { padding-top: 8px; color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-body); font-weight: 500; } .goal-overview .goal-work-row.is-compact { grid-template-columns: 14px minmax(0, 1fr); padding: 8px 0; gap: 6px; } -.goal-work-row.is-compact strong { font-size: var(--text-caption); line-height: var(--leading-caption); } +.goal-work-row.is-compact strong { font-size: var(--text-body); line-height: var(--leading-body); } .goal-work-row.is-compact .goal-work-badge { grid-column: 2; } -.goal-rail-section > button:not(.goal-work-row), .goal-material-activity > button { width: 100%; border: 0; border-bottom: 1px solid var(--pw-line); border-radius: 0; background: transparent; padding: 8px 0; font-size: var(--text-caption); } +.goal-rail-section > button:not(.goal-work-row), .goal-material-activity > button { width: 100%; border: 0; border-bottom: 1px solid var(--pw-line); border-radius: 0; background: transparent; padding: 12px 0; font-size: var(--text-body); line-height: var(--leading-body); } +.goal-rail-attention > button { display: grid; grid-template-columns: 17px minmax(0, 1fr) 14px; gap: 10px; align-items: start; } +.goal-rail-attention > button > svg:first-child { color: var(--pw-caution); } +.goal-rail-attention > button > svg:last-child { color: var(--pw-faint); } +.goal-rail-attention > button > span { overflow-wrap: anywhere; } .goal-material-activity { display: grid; gap: 6px; } .goal-material-activity small { display: block; color: var(--pw-muted); margin-top: 4px; } .goal-all-work .personal-task-lane-scroll { max-height: 400px; } @@ -47,8 +61,13 @@ .goal-all-work .personal-completed-row button[aria-pressed="true"] { box-shadow: inset 2px 0 var(--pw-blue); } .goal-work-ribbon .is-needs_you { background: var(--pw-red); } @media (max-width: 1200px) { - .goal-workspace-composition { grid-template-columns: minmax(0, 1fr); } - .goal-all-work { border-left: 0; border-top: 1px solid var(--pw-line); padding: 20px 0 0; } + .goal-workspace-composition.has-rail { grid-template-columns: minmax(0, 1fr); gap: 24px; } + .goal-workspace-document, .goal-workspace-rail { display: contents; } + .goal-overview-direction { order: 0; } + .goal-overview-attention { order: 1; } + .goal-now { order: 2; } + .goal-workspace-inventory { display: none; } + .goal-work-rail-dialog .goal-all-work { padding: 0; } } @media (max-width: 600px) { .goal-overview .goal-work-row { grid-template-columns: 16px minmax(0, 1fr) auto; } @@ -60,9 +79,13 @@ /* Drawer and sidebar widths change the reading column without a viewport resize. */ .goal-overview { container-type: inline-size; } @container (max-width: 920px) { - .goal-workspace-composition { grid-template-columns: minmax(0, 1fr); } - .goal-all-work { border-left: 0; border-top: 1px solid var(--pw-line); padding: 20px 0 0; } - .goal-overview-summary.has-attention { grid-template-columns: minmax(0, 1fr); } + .goal-workspace-composition.has-rail { grid-template-columns: minmax(0, 1fr); gap: 24px; } + .goal-workspace-document, .goal-workspace-rail { display: contents; } + .goal-overview-direction { order: 0; } + .goal-overview-attention { order: 1; } + .goal-now { order: 2; } + .goal-workspace-inventory { display: none; } + .goal-work-rail-dialog .goal-all-work { padding: 0; } } .goal-overview .goal-direction-toggle { display: none; } .personal-channel-header .personal-channel-actions > .goal-all-work-trigger { display: inline-flex; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.tsx index ea1ed59615..83f7e21d84 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-workspace-work.tsx @@ -1,5 +1,5 @@ import {ContextHelp} from "./context-help"; -import {Circle, CircleAlert, CircleCheck, CircleHelp, Clock, Hand, Info, Play, Plus, Search} from "lucide-react"; +import {ArrowRight, ChevronDown, Circle, CircleAlert, CircleCheck, CircleHelp, Clock, Hand, Info, List, Play, Plus, Search} from "lucide-react"; import {useEffect, useMemo, useRef, useState, type ReactNode} from "react"; import type {GoalWorkDisplayClass, GoalWorkspaceDigest} from "../../../../../../loopx/control_plane/presentation/goal_workspace_digest.js"; import type {WorkspaceActionPreview, WorkspaceActionPreviewRequest, WorkspaceDrawerSelection, WorkspaceGoal, WorkspaceModel, WorkspaceTimelineItem} from "./personal-workspace-model"; @@ -62,8 +62,9 @@ function AddTask({goal, proposals, onPreview, onSelect}: { ; } -export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoId, onSelect, board, map, +export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoId, onSelect, board, map, brief, attention: attentionContent, readOnly, active, historyRefreshRevision, proposals, onPreview, railOpen, onCloseRail, onOpenAgentConversation}: { + brief: ReactNode; attention: ReactNode; onOpenAgentConversation?: (agentId: string) => void; railOpen: boolean; onCloseRail: () => void; goal: WorkspaceGoal; digest: GoalWorkspaceDigest; items: WorkspaceTimelineItem[]; userTodos: WorkspaceModel["userTodos"]; @@ -85,6 +86,9 @@ export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoI }; const [lens, setLens] = useState<"owner" | "board" | "map">("owner"); const [visited, setVisited] = useState(() => new Set(["owner"])); + const [inventoryOpen, setInventoryOpen] = useState(false); + const [inventoryAvailable, setInventoryAvailable] = useState(false); + const inventory = useRef(null); const [railTab, setRailTab] = useState<"work" | "activity">("work"); const [filter, setFilter] = useState<"all" | "needsYou" | "open" | "blocked" | "done">("all"); const [query, setQuery] = useState(""); @@ -117,12 +121,24 @@ export function GoalWorkspaceWork({goal, digest, items, userTodos, selectedTodoI const quietWork = digest.work.coverage === "complete" && digest.work.total === 0 && digest.needsYou.availability === "ok" && digest.needsYou.blocking === 0 && digest.needsYou.optional === 0 && !attention.length; + const hasRail = Boolean(attentionContent) || !quietWork; + useEffect(() => { + const node = inventory.current; + if (!node) {setInventoryAvailable(false); return;} + // Follow the rendered CSS layout, including container-driven reflow, rather + // than duplicating its breakpoints in JavaScript. Hidden history must not read. + const measure = () => setInventoryAvailable(node.getClientRects().length > 0); + const observer = new ResizeObserver(measure); + measure(); + observer.observe(node); + return () => observer.disconnect(); + }, [quietWork]); const sectionLabels = {needsYou: zh ? "等待决定" : "Needs a decision", open: zh ? "未完成" : "Open", blocked: zh ? "受阻与延后" : "Blocked and deferred", done: zh ? "已完成" : "Done"}; const activity = items.filter(item => (item.kind === "run" && item.run.goalId === goal.goalId && ["completed", "failed", "interrupted"].includes(item.run.status)) || (item.kind === "output" && item.output.goalId === goal.goalId) || (item.kind === "proposal" && item.proposal.goalId === goal.goalId && item.proposal.status === "applied" && item.proposal.actionKind === "gate.resolve")); - const rail =