# Discord Style Project Workbench UI Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build the confirmed shadcn/ui-inspired Svelte workbench UI with a full login page, dynamic project rail, project-scoped channel sidebar, channel-specific content pages, and a right-side object inspector. **Architecture:** Keep the implementation frontend-focused under `apps/web/src`. `App.svelte` owns authentication state and switches between login and workbench. `ProjectWorkbench.svelte` owns selected project, selected channel, selected object, and passes focused data into small channel components. Mock project/channel data lives in a local typed data module so later backend API wiring can replace it without reshaping every component. **Tech Stack:** Svelte 5, TypeScript, Vite, Vitest, Testing Library for Svelte, Playwright, local CSS. Visual style follows root `design.md`; do not add React or shadcn/ui runtime dependencies. ## Global Constraints - Frontend only uses Svelte; do not add React dependencies or React components. - UI can visually reference shadcn/ui, but must not add shadcn/ui as a dependency. - Web app is desktop-first while remaining usable on mobile browsers. - Feature code lives under `apps/web/src/features`. - Login page must allow users to enter server IP address or domain. - Browser-style workspace state is separate from editor drafts and AI input drafts. - MVP uses Markdown for note editing. - Project tags remain project-scoped; do not introduce global tags. - AI may not create official objects without user confirmation. - Cron plan UI is plan/reminder management only; do not implement autonomous Agent execution. - Custom channels are URL shortcuts with title, icon, URL, and sort order. - Web verification runs from `apps/web`: `npm test -- --run`, `npm run build`, and Playwright when main workflow changes. --- ## File Structure Create or modify these files: - Modify: `apps/web/src/app/App.svelte` Owns app-level login/workbench switching, persisted API base URL, and mock login session. - Modify: `apps/web/src/features/auth/ServerLogin.svelte` Becomes a complete login page with server address, account, password, submit, and error/connection state. - Create: `apps/web/src/features/workbench/types.ts` Defines `WorkbenchProject`, `WorkbenchChannel`, `ChannelType`, channel object types, inspector model, and mock item interfaces. - Create: `apps/web/src/features/workbench/mockData.ts` Provides typed dynamic projects, project-specific system channels, custom URL channels, tags, recent sessions, inbox messages, tasks, notes/sources, cron plans, and AI sessions. - Create: `apps/web/src/features/workbench/ProjectWorkbench.svelte` Main logged-in shell with topbar, project rail, channel sidebar, main channel content, and inspector. - Create: `apps/web/src/features/workbench/ProjectRail.svelte` Dynamic project list with active project state and create-project action. - Create: `apps/web/src/features/workbench/ProjectChannelSidebar.svelte` Project-scoped channel list, tags, custom channels, recent sessions, and counts. - Create: `apps/web/src/features/workbench/WorkspaceTopbar.svelte` Logo, global search, navigation actions, account action. - Create: `apps/web/src/features/workbench/ChannelContent.svelte` Dispatches selected channel to the matching channel component. - Create: `apps/web/src/features/workbench/ObjectInspector.svelte` Right panel with Discussion, Properties, and More tabs. - Create: `apps/web/src/features/workbench/channels/OverviewChannel.svelte` - Create: `apps/web/src/features/workbench/channels/InboxChannel.svelte` - Create: `apps/web/src/features/workbench/channels/TasksChannel.svelte` - Create: `apps/web/src/features/workbench/channels/AISessionsChannel.svelte` - Create: `apps/web/src/features/workbench/channels/NotesSourcesChannel.svelte` - Create: `apps/web/src/features/workbench/channels/CronChannel.svelte` - Create: `apps/web/src/features/workbench/channels/CustomLinkChannel.svelte` - Create: `apps/web/src/features/workbench/ProjectWorkbench.test.ts` - Modify: `apps/web/e2e/project-workbench.spec.ts` - Modify: `apps/web/src/index.css` --- ## Task 1: Login Page And App-Level Session **Files:** - Modify: `apps/web/src/features/auth/ServerLogin.svelte` - Modify: `apps/web/src/app/App.svelte` - Test: `apps/web/src/features/auth/ServerLogin.test.ts` **Interfaces:** - Produces: `ServerLogin` Svelte component event `login: { apiBase: string; account: string }` - Produces: app state fields `apiBase: string`, `currentUser: { account: string } | null` - Consumes: `localStorage['apiBase']` - [ ] **Step 1: Write failing login component tests** Create `apps/web/src/features/auth/ServerLogin.test.ts`: ```ts import '@testing-library/jest-dom/vitest'; import { fireEvent, render, screen } from '@testing-library/svelte'; import { describe, expect, it, vi } from 'vitest'; import ServerLogin from './ServerLogin.svelte'; describe('ServerLogin', () => { it('renders server, account, and password fields', () => { render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } }); expect(screen.getByLabelText('Server IP or domain')).toBeInTheDocument(); expect(screen.getByLabelText('Email or username')).toBeInTheDocument(); expect(screen.getByLabelText('Password')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument(); }); it('normalizes server address and emits login details', async () => { const onLogin = vi.fn(); render(ServerLogin, { props: { apiBase: 'localhost:8080', onLogin, }, }); await fireEvent.input(screen.getByLabelText('Server IP or domain'), { target: { value: '10.0.0.12:8080' }, }); await fireEvent.input(screen.getByLabelText('Email or username'), { target: { value: 'david@example.com' }, }); await fireEvent.input(screen.getByLabelText('Password'), { target: { value: 'secret' }, }); await fireEvent.click(screen.getByRole('button', { name: 'Log in' })); expect(onLogin).toHaveBeenCalledWith({ apiBase: 'http://10.0.0.12:8080', account: 'david@example.com', }); }); it('shows an error when account or password is missing', async () => { render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } }); await fireEvent.click(screen.getByRole('button', { name: 'Log in' })); expect(screen.getByText('Enter an account and password.')).toBeInTheDocument(); }); }); ``` - [ ] **Step 2: Run the new test to verify it fails** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/auth/ServerLogin.test.ts ``` Expected: FAIL because `ServerLogin` does not render account/password fields and does not expose `onLogin`. - [ ] **Step 3: Replace `ServerLogin.svelte` with the complete login page** Implement `apps/web/src/features/auth/ServerLogin.svelte`: ```svelte
``` - [ ] **Step 4: Update `App.svelte` to switch from login to workbench** Temporarily render a simple logged-in placeholder; the full workbench arrives in Task 3. ```svelte {#if currentUser}

SenlinAI Workbench

Signed in as {currentUser.account}

{:else} {/if} ``` - [ ] **Step 5: Run tests** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/auth/ServerLogin.test.ts ``` Expected: PASS. - [ ] **Step 6: Commit** Run: ```powershell Set-Location D:\work\senlinai\agent git add apps\web\src\features\auth\ServerLogin.svelte apps\web\src\features\auth\ServerLogin.test.ts apps\web\src\app\App.svelte git commit -m "feat: add full workbench login page" ``` --- ## 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" ``` --- ## Task 3: Workbench Shell, Project Rail, Channel Sidebar, And Topbar **Files:** - Create: `apps/web/src/features/workbench/ProjectWorkbench.svelte` - Create: `apps/web/src/features/workbench/ProjectRail.svelte` - Create: `apps/web/src/features/workbench/ProjectChannelSidebar.svelte` - Create: `apps/web/src/features/workbench/WorkspaceTopbar.svelte` - Modify: `apps/web/src/app/App.svelte` - Test: `apps/web/src/features/workbench/ProjectWorkbench.test.ts` **Interfaces:** - Consumes: `workbenchProjects: WorkbenchProject[]` - Consumes: `getProjectWorkspace(projectID: number): ProjectWorkspace` - Produces: `ProjectWorkbench` prop `currentUser: { account: string }` - Produces: selected project and selected channel rendered with accessible names. - [ ] **Step 1: Write failing workbench shell test** Create `apps/web/src/features/workbench/ProjectWorkbench.test.ts`: ```ts import '@testing-library/jest-dom/vitest'; import { fireEvent, render, screen } from '@testing-library/svelte'; import { describe, expect, it } from 'vitest'; import ProjectWorkbench from './ProjectWorkbench.svelte'; describe('ProjectWorkbench', () => { it('renders project rail, channel sidebar, topbar, and inspector', () => { render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } }); expect(screen.getByLabelText('Project list')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Project A1' })).toHaveAttribute('aria-pressed', 'true'); expect(screen.getByLabelText('Project channels')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Message Flow 36' })).toBeInTheDocument(); expect(screen.getByLabelText('Global search')).toBeInTheDocument(); expect(screen.getByLabelText('Object inspector')).toBeInTheDocument(); }); it('refreshes channels when switching projects', async () => { render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } }); await fireEvent.click(screen.getByRole('button', { name: 'Project A2' })); expect(screen.getByRole('button', { name: 'Project A2' })).toHaveAttribute('aria-pressed', 'true'); expect(screen.getByText('Ops Dashboard')).toBeInTheDocument(); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/ProjectWorkbench.test.ts ``` Expected: FAIL because workbench components do not exist. - [ ] **Step 3: Create shell components** Implement `ProjectRail.svelte`, `ProjectChannelSidebar.svelte`, and `WorkspaceTopbar.svelte` with explicit props and accessible buttons: ```svelte ``` ```svelte ``` ```svelte
SenlinAI
``` - [ ] **Step 4: Create `ProjectWorkbench.svelte` and wire into `App.svelte`** Create `ProjectWorkbench.svelte`: ```svelte

{workspace.channels.find((channel) => channel.id === selectedChannelID)?.title}

``` Modify `App.svelte` to import and render `ProjectWorkbench` when logged in: ```svelte {#if currentUser} {:else} {/if} ``` - [ ] **Step 5: Run test** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/ProjectWorkbench.test.ts ``` Expected: PASS. - [ ] **Step 6: Commit** Run: ```powershell Set-Location D:\work\senlinai\agent git add apps\web\src\app\App.svelte apps\web\src\features\workbench git commit -m "feat: add project channel workbench shell" ``` --- ## Task 4: Channel Content Templates And Object Inspector **Files:** - Create: `apps/web/src/features/workbench/ChannelContent.svelte` - Create: `apps/web/src/features/workbench/ObjectInspector.svelte` - Create: channel components under `apps/web/src/features/workbench/channels/` - Modify: `apps/web/src/features/workbench/ProjectWorkbench.svelte` - Test: `apps/web/src/features/workbench/ChannelContent.test.ts` **Interfaces:** - Consumes: `ProjectWorkspace` - Consumes: selected `WorkbenchChannel` - Produces: `onInspect(item: InspectorItem): void` - Produces: per-channel headings and selectable records. - [ ] **Step 1: Write failing channel content test** Create `apps/web/src/features/workbench/ChannelContent.test.ts`: ```ts import '@testing-library/jest-dom/vitest'; import { fireEvent, render, screen } from '@testing-library/svelte'; import { describe, expect, it } from 'vitest'; import ChannelContent from './ChannelContent.svelte'; import { getProjectWorkspace } from './mockData'; describe('ChannelContent', () => { const workspace = getProjectWorkspace(1); it('renders different templates for system and custom channels', () => { for (const type of ['overview', 'inbox', 'tasks', 'ai_sessions', 'notes_sources', 'cron', 'custom_link'] as const) { const channel = workspace.channels.find((item) => item.type === type); if (!channel) throw new Error(`missing ${type}`); render(ChannelContent, { props: { workspace, channel, onInspect: () => {} } }); } expect(screen.getByText('Open external channel')).toBeInTheDocument(); }); it('sends selected task details to inspector', async () => { let inspectedTitle = ''; const channel = workspace.channels.find((item) => item.type === 'tasks'); if (!channel) throw new Error('missing task channel'); render(ChannelContent, { props: { workspace, channel, onInspect: (item) => { inspectedTitle = item.title; }, }, }); await fireEvent.click(screen.getByRole('button', { name: 'Inspect Confirm homepage information architecture' })); expect(inspectedTitle).toBe('Confirm homepage information architecture'); }); }); ``` - [ ] **Step 2: Run test to verify it fails** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/ChannelContent.test.ts ``` Expected: FAIL because `ChannelContent.svelte` does not exist. - [ ] **Step 3: Implement `ChannelContent.svelte` dispatcher** Create a dispatcher that selects the channel component by `channel.type`: ```svelte {#if channel.type === 'overview'} {:else if channel.type === 'inbox'} {:else if channel.type === 'tasks'} {:else if channel.type === 'ai_sessions'} {:else if channel.type === 'notes_sources'} {:else if channel.type === 'cron'} {:else} {/if} ``` - [ ] **Step 4: Implement channel components** Each channel component should render its own page template and call `onInspect` for selectable records. Example for `TasksChannel.svelte`: ```svelte

Work Plan

Tasks

{#each tasks as task}

{task.title}

{task.summary}

{task.owner} · {task.due} · #{task.tag}
{/each}
``` Implement the remaining channel components with the same pattern: - `OverviewChannel.svelte`: metrics for inbox, tasks, AI sessions, notes/sources, cron plans. - `InboxChannel.svelte`: email-like message list with source, title, summary, status, tag, time. - `AISessionsChannel.svelte`: session list plus selected conversation summary area. - `NotesSourcesChannel.svelte`: file-manager-like list of note/file/link records. - `CronChannel.svelte`: scheduled task rows with enabled state, schedule, next run, last result. - `CustomLinkChannel.svelte`: external URL page with `Open external channel` link and copy-style button. - [ ] **Step 5: Implement `ObjectInspector.svelte` and wire it into `ProjectWorkbench.svelte`** Create `ObjectInspector.svelte`: ```svelte ``` Update `ProjectWorkbench.svelte` so `ChannelContent` receives the selected channel and `ObjectInspector` receives selected item. - [ ] **Step 6: Run channel tests** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/ChannelContent.test.ts src/features/workbench/ProjectWorkbench.test.ts ``` Expected: PASS. - [ ] **Step 7: Commit** Run: ```powershell Set-Location D:\work\senlinai\agent git add apps\web\src\features\workbench git commit -m "feat: add workbench channel templates" ``` --- ## Task 5: shadcn/ui-Inspired Visual System In CSS **Files:** - Modify: `apps/web/src/index.css` - Test: `apps/web/src/features/workbench/ProjectWorkbench.test.ts` **Interfaces:** - Consumes: class names introduced in Tasks 1-4. - Produces: stable desktop layout, responsive mobile layout, accessible focus states. - [ ] **Step 1: Add a test assertion for active state semantics** Extend `ProjectWorkbench.test.ts` with: ```ts it('marks active channel with aria-pressed', async () => { render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } }); await fireEvent.click(screen.getByRole('button', { name: 'Work Plan 8' })); expect(screen.getByRole('button', { name: 'Work Plan 8' })).toHaveAttribute('aria-pressed', 'true'); }); ``` - [ ] **Step 2: Run test** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run src/features/workbench/ProjectWorkbench.test.ts ``` Expected: PASS before visual CSS work; this protects state semantics while styling changes. - [ ] **Step 3: Replace `index.css` with workbench visual system** Implement CSS tokens and component classes: ```css :root { color: #18181b; background: #f4f4f5; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; --background: #f4f4f5; --panel: #ffffff; --panel-muted: #fafafa; --border: #d4d4d8; --text: #18181b; --muted: #71717a; --primary: #0f7ae5; --primary-strong: #0969c8; --success: #15803d; --danger: #b91c1c; --radius: 8px; } * { box-sizing: border-box; } body { margin: 0; min-width: 320px; min-height: 100vh; } button, input, textarea, select { font: inherit; } button { cursor: pointer; } button:focus-visible, input:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } .login-page { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: var(--background); } .login-panel { width: min(420px, 100%); border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); padding: 24px; } .server-login, .login-heading, .channel-page, .object-inspector, .channel-sidebar { display: grid; gap: 12px; } .server-login input, .search-box input { width: 100%; border: 1px solid var(--border); border-radius: 6px; padding: 9px 10px; background: white; } .server-login button, .primary-action { border: 1px solid var(--primary); border-radius: 6px; background: var(--primary); color: white; padding: 9px 12px; } .workbench-shell { min-height: 100vh; display: grid; grid-template-rows: 64px 1fr; } .workspace-topbar { display: grid; grid-template-columns: 160px minmax(240px, 1fr) auto; align-items: center; gap: 16px; border-bottom: 1px solid var(--border); background: var(--panel); padding: 0 16px; } .workbench-body { min-height: 0; display: grid; grid-template-columns: 76px 280px minmax(0, 1fr) 320px; } .project-rail, .channel-sidebar, .object-inspector { border-right: 1px solid var(--border); background: var(--panel); } .project-rail { display: grid; align-content: start; gap: 10px; padding: 12px; } .project-rail button { min-width: 48px; min-height: 48px; border: 1px solid var(--border); border-radius: 999px; background: var(--panel-muted); } .project-rail button.active, .channel-group button.active { border-color: var(--primary); color: var(--primary); background: #eff6ff; } .channel-sidebar, .object-inspector, .channel-stage { padding: 16px; overflow: auto; } .channel-group, .recent-sessions, .task-list, .record-list { display: grid; gap: 8px; } .channel-group button, .recent-sessions button, .task-card, .record-row, .cron-row { width: 100%; border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); padding: 10px 12px; } .task-card, .record-row, .cron-row { display: grid; grid-template-columns: auto 1fr auto; gap: 12px; align-items: start; } .task-card.completed { color: var(--muted); background: var(--panel-muted); } .inspector-tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; } .inspector-tabs button[aria-selected='true'] { border-color: var(--primary); color: var(--primary); } @media (max-width: 920px) { .workspace-topbar { grid-template-columns: 1fr; height: auto; padding: 12px; } .workbench-shell { grid-template-rows: auto 1fr; } .workbench-body { grid-template-columns: 1fr; } .project-rail, .channel-sidebar, .object-inspector { border-right: 0; border-bottom: 1px solid var(--border); } } ``` - [ ] **Step 4: Run component tests and build** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run npm run build ``` Expected: both PASS. - [ ] **Step 5: Commit** Run: ```powershell Set-Location D:\work\senlinai\agent git add apps\web\src\index.css apps\web\src\features\workbench\ProjectWorkbench.test.ts git commit -m "style: apply workbench visual system" ``` --- ## Task 6: Playwright Smoke Flow **Files:** - Modify: `apps/web/e2e/project-workbench.spec.ts` **Interfaces:** - Consumes: login form labels from Task 1. - Consumes: project/channel labels from Tasks 2-4. - Produces: smoke coverage for login, dynamic project switching, channel template switching, custom URL channel. - [ ] **Step 1: Replace Playwright smoke tests** Update `apps/web/e2e/project-workbench.spec.ts`: ```ts import { expect, test } from '@playwright/test'; test('login and project channel workbench flow', async ({ page }) => { await page.goto('/'); await expect(page.getByLabel('Server IP or domain')).toBeVisible(); await page.getByLabel('Server IP or domain').fill('localhost:8080'); await page.getByLabel('Email or username').fill('david@example.com'); await page.getByLabel('Password').fill('secret'); await page.getByRole('button', { name: 'Log in' }).click(); await expect(page.getByLabel('Project list')).toBeVisible(); await expect(page.getByRole('button', { name: 'Project A1' })).toHaveAttribute('aria-pressed', 'true'); await expect(page.getByRole('button', { name: 'Message Flow 36' })).toBeVisible(); await page.getByRole('button', { name: 'Work Plan 8' }).click(); await expect(page.getByText('Tasks')).toBeVisible(); await expect(page.getByRole('button', { name: 'Inspect Confirm homepage information architecture' })).toBeVisible(); await page.getByRole('button', { name: 'Project A2' }).click(); await expect(page.getByRole('button', { name: 'Project A2' })).toHaveAttribute('aria-pressed', 'true'); await expect(page.getByText('Ops Dashboard')).toBeVisible(); await page.getByRole('button', { name: 'Ops Dashboard' }).click(); await expect(page.getByText('Open external channel')).toBeVisible(); }); ``` - [ ] **Step 2: Run Playwright test** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npx playwright test ``` Expected: PASS. - [ ] **Step 3: Run full web verification** Run: ```powershell Set-Location D:\work\senlinai\agent\apps\web npm test -- --run npm run build npx playwright test ``` Expected: all PASS. - [ ] **Step 4: Commit** Run: ```powershell Set-Location D:\work\senlinai\agent git add apps\web\e2e\project-workbench.spec.ts git commit -m "test: cover project channel workbench flow" ``` --- ## Self-Review Spec coverage: - Login page with server IP/domain, account, password, status, and errors: Task 1. - Dynamic project rail and project-scoped channel refresh: Tasks 2 and 3. - System channels with different page templates: Task 4. - Message flow similar to email: Task 4. - Work plan Todo card style with complete/incomplete state: Task 4. - AI sessions with session list and details: Task 4. - Notes and sources file/attachment management style: Task 4. - Cron plan/reminder management without autonomous Agent execution: Task 4 and Global Constraints. - Custom channel with title, icon, URL: Tasks 2 and 4. - Right inspector with Discussion, Properties, More: Task 4. - shadcn/ui-inspired visual system without React/shadcn dependency: Task 5. - Playwright workflow coverage: Task 6. Completeness scan: - The plan avoids unresolved markers, deferred-work wording, and vague validation instructions. - Each code-writing step includes concrete file content or concrete implementation shape with exact paths. Type consistency: - `ChannelType`, `WorkbenchChannel`, `ProjectWorkspace`, and `InspectorItem` are introduced in Task 2 and consumed consistently in Tasks 3 and 4. - `onInspect(item: InspectorItem): void` is the only inspector selection interface. - `onLogin({ apiBase, account })` is the only login completion interface.