diff --git a/packages/web/src/app/console-tokens.css b/packages/web/src/app/console-tokens.css index 859a4a85e9..d8c1e8e4c5 100644 --- a/packages/web/src/app/console-tokens.css +++ b/packages/web/src/app/console-tokens.css @@ -40,6 +40,8 @@ /* ============ Shadows — elevation tokens(dark mode 自动 inset 高光) ============ */ --console-shadow-soft: var(--shadow-elevation-1); + /* Card lift — brown-tinted, used by dashboard cards across Mission Hub */ + --console-card-shadow: 0 8px 22px oklch(0.22 0.02 50 / 0.04); /* Rail (ActivityBar) active/hover lift — wider/softer than elevation-1, brown-tinted */ --console-rail-shadow: 0 5px 14px oklch(0.22 0.02 50 / 0.07); @@ -198,6 +200,8 @@ [data-theme="dark"] { /* Dark rail shadow — deeper, no brown tint to avoid muddy look on dark surfaces */ --console-rail-shadow: 0 5px 14px oklch(0 0 0 / 0.4), inset 0 1px 0 oklch(1 0 0 / 0.08); + /* Dark card lift — deeper, no brown tint */ + --console-card-shadow: 0 8px 22px oklch(0 0 0 / 0.35), inset 0 1px 0 oklch(1 0 0 / 0.06); /* Mission Control dark */ --mc-status-open-dot: #b8a88f; diff --git a/packages/web/src/components/__tests__/team-home-adapter.test.ts b/packages/web/src/components/__tests__/team-home-adapter.test.ts new file mode 100644 index 0000000000..f560c0df41 --- /dev/null +++ b/packages/web/src/components/__tests__/team-home-adapter.test.ts @@ -0,0 +1,127 @@ +import type { BacklogItem, CatId } from '@cat-cafe/shared'; +import { describe, expect, it } from 'vitest'; +import { adaptTeamHomeData } from '../mission-control/team-home/adapter'; +import { teamHomeFixture } from '../mission-control/team-home/fixture'; + +function catId(id: string): CatId { + return id as CatId; +} + +function makeBacklogItem(overrides: Partial = {}): BacklogItem { + const now = Date.UTC(2026, 5, 18, 9, 0, 0); + return { + id: 'backlog-f049', + userId: 'u_test', + title: '[F049] Mission Hub', + summary: 'Mission Hub work', + priority: 'p1', + tags: ['source:docs-backlog', 'feature:f049'], + status: 'dispatched', + createdBy: 'user', + createdAt: now - 10_000, + updatedAt: now - 1_000, + dispatchedAt: now - 5_000, + dispatchedThreadId: 'thread-f049', + dispatchedThreadPhase: 'coding', + audit: [{ id: 'audit-dispatched', action: 'dispatched', actor: { kind: 'user', id: 'u_test' }, timestamp: now }], + ...overrides, + }; +} + +describe('adaptTeamHomeData', () => { + it('derives the active feature from docs backlog feature tags', () => { + const data = adaptTeamHomeData({ items: [makeBacklogItem()] }); + + expect(data.mission.activeFeatureId).toBe('F049'); + expect(data.mission.truthSourceUrl).toBe('/api/backlog/feature-doc-detail?featureId=F049'); + }); + + it('projects active lease and thread state into the baton and team cards', () => { + const acquiredAt = Date.UTC(2026, 5, 18, 9, 5, 0); + const heartbeatAt = Date.UTC(2026, 5, 18, 9, 20, 0); + const lastActiveAt = Date.UTC(2026, 5, 18, 9, 25, 0); + const item = makeBacklogItem({ + title: 'Team Home MVP', + lease: { + ownerCatId: catId('codex'), + state: 'active', + acquiredAt, + heartbeatAt, + expiresAt: Date.UTC(2026, 5, 18, 10, 0, 0), + }, + }); + + const data = adaptTeamHomeData({ + items: [item], + threadsByBacklogId: { + [item.id]: { + lastActiveAt, + participants: [catId('codex'), catId('claude')], + }, + }, + }); + + expect(data.baton.holder).toBe('codex'); + expect(data.baton.scope).toBe('Team Home MVP'); + expect(data.baton.since).toBe(new Date(acquiredAt).toISOString()); + expect(data.baton.nextStep).toBe('@codex 正在实现中'); + + expect(data.team.find((member) => member.id === 'codex')?.currentContext).toBe('持球:Team Home MVP'); + expect(data.team.find((member) => member.id === 'codex')?.lastActiveAt).toBe(new Date(lastActiveAt).toISOString()); + expect(data.team.find((member) => member.id === 'claude')?.currentContext).toBe('参与:Team Home MVP'); + expect(data.team.find((member) => member.id === 'claude')?.lastActiveAt).toBe(new Date(lastActiveAt).toISOString()); + }); + + it('does not treat audit lifecycle logs as evidence items', () => { + const data = adaptTeamHomeData({ + items: [ + makeBacklogItem({ + audit: [ + { id: 'audit-created', action: 'created', actor: { kind: 'user', id: 'u_test' }, timestamp: 1 }, + { id: 'audit-dispatched', action: 'dispatched', actor: { kind: 'user', id: 'u_test' }, timestamp: 2 }, + { id: 'audit-heartbeat', action: 'lease_heartbeat', actor: { kind: 'cat', id: 'codex' }, timestamp: 3 }, + ], + }), + ], + }); + + expect(data.missions[0]?.evidenceCount).toBeUndefined(); + expect(data.missions[0]?.requiredEvidence).toBeUndefined(); + }); + + it('prefers active lease over dispatched over approved regardless of item order', () => { + const approved = makeBacklogItem({ + id: 'approved-1', + status: 'approved', + title: 'Approved Mission', + }); + const dispatchedWithLease = makeBacklogItem({ + id: 'dispatched-1', + status: 'dispatched', + title: 'Dispatched With Lease', + lease: { + ownerCatId: catId('claude'), + state: 'active', + acquiredAt: Date.UTC(2026, 5, 18, 9, 10, 0), + heartbeatAt: Date.UTC(2026, 5, 18, 9, 20, 0), + expiresAt: Date.UTC(2026, 5, 18, 10, 0, 0), + }, + }); + + const data = adaptTeamHomeData({ items: [approved, dispatchedWithLease] }); + + expect(data.baton.scope).toBe('Dispatched With Lease'); + expect(data.baton.holder).toBe('claude'); + expect(data.mission.activeFeatureId).toBe('F049'); + }); + + it('only treats approved or dispatched items as active work', () => { + const suggested = makeBacklogItem({ id: 'suggested-1', status: 'suggested' }); + const done = makeBacklogItem({ id: 'done-1', status: 'done' }); + const data = adaptTeamHomeData({ items: [suggested, done] }); + + expect(data.missions).toHaveLength(0); + expect(data.baton.scope).toBe(teamHomeFixture.baton.scope); + expect(data.mission.activeFeatureId).toBe('—'); + }); +}); diff --git a/packages/web/src/components/mission-control/MissionControlPage.tsx b/packages/web/src/components/mission-control/MissionControlPage.tsx index 4d95781d71..f099cf0ca9 100644 --- a/packages/web/src/components/mission-control/MissionControlPage.tsx +++ b/packages/web/src/components/mission-control/MissionControlPage.tsx @@ -13,6 +13,7 @@ import { ImportProjectModal } from './ImportProjectModal'; import { QuickCreateForm } from './QuickCreateForm'; import { SuggestionDrawer } from './SuggestionDrawer'; import { ThreadSituationPanel } from './ThreadSituationPanel'; +import { TeamHomePanel } from './team-home/TeamHomePanel'; import { WorkflowSopPanel } from './WorkflowSopPanel'; interface BacklogListResponse { @@ -470,6 +471,18 @@ export function MissionControlPage() { )}
+