Files
agent/.superpowers/sdd/task-3-brief.md
2026-07-20 08:02:20 +08:00

8.3 KiB

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:

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:

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:

<!-- apps/web/src/features/workbench/ProjectRail.svelte -->
<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>
<!-- apps/web/src/features/workbench/ProjectChannelSidebar.svelte -->
<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">鈿?/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>
<!-- apps/web/src/features/workbench/WorkspaceTopbar.svelte -->
<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">鈫?/button>
    <button aria-label="Forward">鈫?/button>
    <button aria-label={`Account ${account}`}>{account.slice(0, 2).toUpperCase()}</button>
  </div>
</header>
  • Step 4: Create ProjectWorkbench.svelte and wire into App.svelte

Create ProjectWorkbench.svelte:

<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>

Modify App.svelte to import and render ProjectWorkbench when logged in:

<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}
  <ProjectWorkbench {currentUser} />
{:else}
  <ServerLogin {apiBase} onLogin={handleLogin} />
{/if}
  • Step 5: Run test

Run:

Set-Location D:\work\senlinai\agent\apps\web
npm test -- --run src/features/workbench/ProjectWorkbench.test.ts

Expected: PASS.

  • Step 6: Commit

Run:

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"