# Review package Task 3

## Commits
95dce81 feat: add project channel workbench shell

## Stat
 apps/web/src/app/App.svelte                        |  6 +--
 .../workbench/ProjectChannelSidebar.svelte         | 52 ++++++++++++++++++++++
 apps/web/src/features/workbench/ProjectRail.svelte | 26 +++++++++++
 .../src/features/workbench/ProjectWorkbench.svelte | 41 +++++++++++++++++
 .../features/workbench/ProjectWorkbench.test.ts    | 26 +++++++++++
 .../src/features/workbench/WorkspaceTopbar.svelte  | 16 +++++++
 6 files changed, 163 insertions(+), 4 deletions(-)

## Diff
diff --git a/apps/web/src/app/App.svelte b/apps/web/src/app/App.svelte
index a5d8dce..e7170ef 100644
--- a/apps/web/src/app/App.svelte
+++ b/apps/web/src/app/App.svelte
@@ -1,21 +1,19 @@
 <script lang="ts">
+  import ProjectWorkbench from '../features/workbench/ProjectWorkbench.svelte';
   import ServerLogin from '../features/auth/ServerLogin.svelte';
 
   let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
   let currentUser: { account: string } | null = null;
 
   function handleLogin(detail: { apiBase: string; account: string }) {
     apiBase = detail.apiBase;
     localStorage.setItem('apiBase', apiBase);
     currentUser = { account: detail.account };
   }
 </script>
 
 {#if currentUser}
-  <main class="workbench-placeholder" aria-label="Project workbench">
-    <h1>SenlinAI Workbench</h1>
-    <p>Signed in as {currentUser.account}</p>
-  </main>
+  <ProjectWorkbench {currentUser} />
 {:else}
   <ServerLogin {apiBase} onLogin={handleLogin} />
 {/if}
diff --git a/apps/web/src/features/workbench/ProjectChannelSidebar.svelte b/apps/web/src/features/workbench/ProjectChannelSidebar.svelte
new file mode 100644
index 0000000..40cb1a0
--- /dev/null
+++ b/apps/web/src/features/workbench/ProjectChannelSidebar.svelte
@@ -0,0 +1,52 @@
+<script lang="ts">
+  import type { AISessionItem, WorkbenchChannel, WorkbenchProject } from './types';
+
+  export let project: WorkbenchProject;
+  export let channels: WorkbenchChannel[];
+  export let selectedChannelID: string;
+  export let tags: string[];
+  export let recentSessions: AISessionItem[];
+  export let onSelectChannel: (channelID: string) => void;
+</script>
+
+<aside class="channel-sidebar" aria-label="Project channels">
+  <header>
+    <div>
+      <p>Current project</p>
+      <h2>{project.name}</h2>
+    </div>
+    <button aria-label="Project settings">&#9881;</button>
+  </header>
+
+  <section class="tag-row" aria-label="Project tags">
+    {#each tags as tag}
+      <button type="button">#{tag}</button>
+    {/each}
+  </section>
+
+  <section class="channel-group">
+    {#each channels as channel}
+      <button
+        class:active={channel.id === selectedChannelID}
+        aria-pressed={channel.id === selectedChannelID}
+        aria-label={`${channel.title}${channel.count ? ` ${channel.count}` : ''}`}
+        on:click={() => onSelectChannel(channel.id)}
+      >
+        <span>{channel.title}</span>
+        {#if channel.count !== undefined}
+          <small>{channel.count}</small>
+        {/if}
+      </button>
+    {/each}
+  </section>
+
+  <section class="recent-sessions" aria-label="Recent sessions">
+    <h3>Recent sessions</h3>
+    {#each recentSessions as session}
+      <button type="button">
+        <span>{session.title}</span>
+        <small>{session.updatedAt}</small>
+      </button>
+    {/each}
+  </section>
+</aside>
diff --git a/apps/web/src/features/workbench/ProjectRail.svelte b/apps/web/src/features/workbench/ProjectRail.svelte
new file mode 100644
index 0000000..d1be341
--- /dev/null
+++ b/apps/web/src/features/workbench/ProjectRail.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+  import type { WorkbenchProject } from './types';
+
+  export let projects: WorkbenchProject[];
+  export let selectedProjectID: number;
+  export let onSelectProject: (projectID: number) => void;
+</script>
+
+<nav class="project-rail" aria-label="Project list">
+  <button class="rail-logo" aria-label="Dashboard">SA</button>
+  {#each projects as project}
+    <button
+      class:active={project.id === selectedProjectID}
+      aria-pressed={project.id === selectedProjectID}
+      aria-label={project.name}
+      title={project.name}
+      on:click={() => onSelectProject(project.id)}
+    >
+      <span>{project.initials}</span>
+      {#if project.unreadCount}
+        <small>{project.unreadCount}</small>
+      {/if}
+    </button>
+  {/each}
+  <button class="add-project" aria-label="Create project">+</button>
+</nav>
diff --git a/apps/web/src/features/workbench/ProjectWorkbench.svelte b/apps/web/src/features/workbench/ProjectWorkbench.svelte
new file mode 100644
index 0000000..4ab9ffd
--- /dev/null
+++ b/apps/web/src/features/workbench/ProjectWorkbench.svelte
@@ -0,0 +1,41 @@
+<script lang="ts">
+  import { getProjectWorkspace, workbenchProjects } from './mockData';
+  import ProjectRail from './ProjectRail.svelte';
+  import ProjectChannelSidebar from './ProjectChannelSidebar.svelte';
+  import WorkspaceTopbar from './WorkspaceTopbar.svelte';
+
+  export let currentUser: { account: string };
+
+  let selectedProjectID = workbenchProjects[0].id;
+  $: workspace = getProjectWorkspace(selectedProjectID);
+  $: selectedChannelID = workspace.channels[0].id;
+
+  function selectProject(projectID: number) {
+    selectedProjectID = projectID;
+  }
+
+  function selectChannel(channelID: string) {
+    selectedChannelID = channelID;
+  }
+</script>
+
+<main class="workbench-shell">
+  <WorkspaceTopbar account={currentUser.account} />
+  <div class="workbench-body">
+    <ProjectRail projects={workbenchProjects} {selectedProjectID} onSelectProject={selectProject} />
+    <ProjectChannelSidebar
+      project={workspace.project}
+      channels={workspace.channels}
+      {selectedChannelID}
+      tags={workspace.tags}
+      recentSessions={workspace.recentSessions}
+      onSelectChannel={selectChannel}
+    />
+    <section class="channel-stage" aria-label="Channel content">
+      <h1>{workspace.channels.find((channel) => channel.id === selectedChannelID)?.title}</h1>
+    </section>
+    <aside class="object-inspector" aria-label="Object inspector">
+      <p>Select an item to inspect discussion, properties, and actions.</p>
+    </aside>
+  </div>
+</main>
diff --git a/apps/web/src/features/workbench/ProjectWorkbench.test.ts b/apps/web/src/features/workbench/ProjectWorkbench.test.ts
new file mode 100644
index 0000000..cf251b3
--- /dev/null
+++ b/apps/web/src/features/workbench/ProjectWorkbench.test.ts
@@ -0,0 +1,26 @@
+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();
+  });
+});
diff --git a/apps/web/src/features/workbench/WorkspaceTopbar.svelte b/apps/web/src/features/workbench/WorkspaceTopbar.svelte
new file mode 100644
index 0000000..c828fcf
--- /dev/null
+++ b/apps/web/src/features/workbench/WorkspaceTopbar.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+  export let account: string;
+</script>
+
+<header class="workspace-topbar">
+  <div class="brand-mark">SenlinAI</div>
+  <label class="search-box">
+    <span>Search</span>
+    <input aria-label="Global search" placeholder="Search projects, tasks, notes, AI sessions" />
+  </label>
+  <div class="topbar-actions">
+    <button aria-label="Back">&#8592;</button>
+    <button aria-label="Forward">&#8594;</button>
+    <button aria-label={`Account ${account}`}>{account.slice(0, 2).toUpperCase()}</button>
+  </div>
+</header>
