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

8.6 KiB

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}

  • SenlinAI Workbench

  • Signed in as {currentUser.account}

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

+

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>

+

  • SA
  • {#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}
    
  • {/each}
  • + +
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>
  • +

    • <ProjectRail projects={workbenchProjects} {selectedProjectID} onSelectProject={selectProject} />
    • <ProjectChannelSidebar
    •  project={workspace.project}
      
    •  channels={workspace.channels}
      
    •  {selectedChannelID}
      
    •  tags={workspace.tags}
      
    •  recentSessions={workspace.recentSessions}
      
    •  onSelectChannel={selectChannel}
      
    • />
    •  <h1>{workspace.channels.find((channel) => channel.id === selectedChannelID)?.title}</h1>
      
    •  <p>Select an item to inspect discussion, properties, and actions.</p>
      

    +

    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>

    +

    • SenlinAI
    • Search
    • <button aria-label={Account ${account}}>{account.slice(0, 2).toUpperCase()}

    +