## Task 2: Workbench Types And Project-Scoped Mock Data **Files:** - Create: `apps/web/src/features/workbench/types.ts` - Create: `apps/web/src/features/workbench/mockData.ts` - Test: `apps/web/src/features/workbench/mockData.test.ts` **Interfaces:** - Produces: `type ChannelType = 'overview' | 'inbox' | 'tasks' | 'ai_sessions' | 'notes_sources' | 'cron' | 'custom_link'` - Produces: `getProjectWorkspace(projectID: number): ProjectWorkspace` - Produces: `workbenchProjects: WorkbenchProject[]` - [ ] **Step 1: Write failing mock data tests** Create `apps/web/src/features/workbench/mockData.test.ts`: ```ts import { describe, expect, it } from 'vitest'; import { getProjectWorkspace, workbenchProjects } from './mockData'; describe('workbench mock data', () => { it('provides dynamic projects', () => { expect(workbenchProjects.length).toBeGreaterThanOrEqual(2); expect(workbenchProjects[0]).toMatchObject({ id: expect.any(Number), name: expect.any(String) }); }); it('loads project-specific channels and counts', () => { const first = getProjectWorkspace(workbenchProjects[0].id); const second = getProjectWorkspace(workbenchProjects[1].id); expect(first.project.id).not.toBe(second.project.id); expect(first.channels.map((channel) => channel.type)).toEqual([ 'overview', 'inbox', 'tasks', 'ai_sessions', 'notes_sources', 'cron', 'custom_link', ]); expect(first.channels.find((channel) => channel.type === 'custom_link')).toMatchObject({ title: expect.any(String), url: expect.stringMatching(/^https?:\/\//), }); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/mockData.test.ts ``` Expected: FAIL because the files do not exist. - [ ] **Step 3: Create `types.ts`** Create `apps/web/src/features/workbench/types.ts`: ```ts export type ChannelType = | 'overview' | 'inbox' | 'tasks' | 'ai_sessions' | 'notes_sources' | 'cron' | 'custom_link'; export type WorkbenchProject = { id: number; name: string; description: string; initials: string; unreadCount: number; }; export type WorkbenchChannel = { id: string; projectID: number; type: ChannelType; title: string; icon: string; count?: number; url?: string; sortOrder: number; }; export type InboxMessage = { id: string; source: string; title: string; summary: string; status: 'open' | 'processed' | 'archived'; tag: string; time: string; }; export type WorkTask = { id: string; title: string; summary: string; completed: boolean; owner: string; due: string; tag: string; }; export type AISessionItem = { id: string; title: string; summary: string; updatedAt: string; references: string[]; }; export type NoteSourceItem = { id: string; kind: 'note' | 'file' | 'link'; title: string; updatedAt: string; tag: string; source: string; }; export type CronPlan = { id: string; title: string; schedule: string; nextRun: string; enabled: boolean; lastResult: string; owner: string; }; export type InspectorItem = { title: string; type: string; description: string; properties: Array<{ label: string; value: string }>; }; export type ProjectWorkspace = { project: WorkbenchProject; channels: WorkbenchChannel[]; tags: string[]; recentSessions: AISessionItem[]; inbox: InboxMessage[]; tasks: WorkTask[]; aiSessions: AISessionItem[]; notesSources: NoteSourceItem[]; cronPlans: CronPlan[]; }; ``` - [ ] **Step 4: Create `mockData.ts`** Create `apps/web/src/features/workbench/mockData.ts` with two project workspaces: ```ts import type { ProjectWorkspace, WorkbenchChannel, WorkbenchProject } from './types'; export const workbenchProjects: WorkbenchProject[] = [ { id: 1, name: 'Project A1', description: 'Client strategy workspace', initials: 'A1', unreadCount: 36 }, { id: 2, name: 'Project A2', description: 'Operations planning workspace', initials: 'A2', unreadCount: 12 }, ]; function systemChannels(projectID: number): WorkbenchChannel[] { return [ { id: `${projectID}-overview`, projectID, type: 'overview', title: 'Overview', icon: 'home', count: 635, sortOrder: 1 }, { id: `${projectID}-inbox`, projectID, type: 'inbox', title: 'Message Flow', icon: 'mail', count: 36, sortOrder: 2 }, { id: `${projectID}-tasks`, projectID, type: 'tasks', title: 'Work Plan', icon: 'list', count: 8, sortOrder: 3 }, { id: `${projectID}-ai`, projectID, type: 'ai_sessions', title: 'AI Sessions', icon: 'sparkles', count: 4, sortOrder: 4 }, { id: `${projectID}-notes`, projectID, type: 'notes_sources', title: 'Notes & Sources', icon: 'file', count: 343, sortOrder: 5 }, { id: `${projectID}-cron`, projectID, type: 'cron', title: 'Cron Plans', icon: 'clock', count: 45, sortOrder: 6 }, { id: `${projectID}-custom-roadmap`, projectID, type: 'custom_link', title: projectID === 1 ? 'Roadmap Board' : 'Ops Dashboard', icon: 'link', url: projectID === 1 ? 'https://example.com/roadmap-a1' : 'https://example.com/ops-a2', sortOrder: 7, }, ]; } const workspaces: ProjectWorkspace[] = workbenchProjects.map((project) => ({ project, channels: systemChannels(project.id), tags: ['all', 'tag1', 'UI', 'knowledge'], recentSessions: [ { id: `${project.id}-session-1`, title: 'Skill setup notes', summary: 'Install and verify Codex skills.', updatedAt: 'Yesterday', references: ['Notes'] }, { id: `${project.id}-session-2`, title: 'Pricing logic analysis', summary: 'Compare decision branches and risks.', updatedAt: '7 days ago', references: ['Tasks', 'Sources'] }, ], inbox: [ { id: `${project.id}-inbox-1`, source: 'Manual', title: 'Collect competitor pricing notes', summary: 'Turn pasted research into structured follow-up tasks.', status: 'open', tag: 'UI', time: '09:32' }, { id: `${project.id}-inbox-2`, source: 'AI', title: 'Meeting summary candidate', summary: 'Review suggested note before saving official object.', status: 'processed', tag: 'knowledge', time: 'Yesterday' }, ], tasks: [ { id: `${project.id}-task-1`, title: 'Confirm homepage information architecture', summary: 'Review channel layout and right inspector behavior.', completed: false, owner: 'David', due: 'Today', tag: 'UI' }, { id: `${project.id}-task-2`, title: 'Archive old planning notes', summary: 'Move outdated notes into processed state.', completed: true, owner: 'Team', due: 'Yesterday', tag: 'knowledge' }, ], aiSessions: [ { id: `${project.id}-ai-1`, title: 'UI prototype critique', summary: 'Discuss Discord-like project channel behavior.', updatedAt: '10 min ago', references: ['ScreenShot.png', 'design.md'] }, { id: `${project.id}-ai-2`, title: 'Task sharing policy', summary: 'Validate explicit object sharing rules.', updatedAt: 'Yesterday', references: ['Tasks'] }, ], notesSources: [ { id: `${project.id}-note-1`, kind: 'note', title: 'Product workbench principles', updatedAt: 'Today', tag: 'knowledge', source: 'Markdown' }, { id: `${project.id}-file-1`, kind: 'file', title: 'ScreenShot.png', updatedAt: 'Today', tag: 'UI', source: 'Attachment' }, { id: `${project.id}-link-1`, kind: 'link', title: 'Reference board', updatedAt: '7 days ago', tag: 'tag1', source: 'URL' }, ], cronPlans: [ { id: `${project.id}-cron-1`, title: 'Weekly inbox review reminder', schedule: '0 9 * * 1', nextRun: 'Next Monday 09:00', enabled: true, lastResult: 'Not run yet', owner: 'David' }, { id: `${project.id}-cron-2`, title: 'Monthly source cleanup', schedule: '0 10 1 * *', nextRun: 'Next month', enabled: false, lastResult: 'Paused', owner: 'Team' }, ], })); export function getProjectWorkspace(projectID: number): ProjectWorkspace { return workspaces.find((workspace) => workspace.project.id === projectID) ?? workspaces[0]; } ``` - [ ] **Step 5: Run test** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/mockData.test.ts ``` Expected: PASS. - [ ] **Step 6: Commit** Run: ```powershell Set-Location D:\work\senlinai\agent git add apps\web\src\features\workbench\types.ts apps\web\src\features\workbench\mockData.ts apps\web\src\features\workbench\mockData.test.ts git commit -m "feat: add project workspace data model" ``` ---