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

7.8 KiB

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:

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:

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:

<script lang="ts">
  import type { InspectorItem, ProjectWorkspace, WorkbenchChannel } from './types';
  import OverviewChannel from './channels/OverviewChannel.svelte';
  import InboxChannel from './channels/InboxChannel.svelte';
  import TasksChannel from './channels/TasksChannel.svelte';
  import AISessionsChannel from './channels/AISessionsChannel.svelte';
  import NotesSourcesChannel from './channels/NotesSourcesChannel.svelte';
  import CronChannel from './channels/CronChannel.svelte';
  import CustomLinkChannel from './channels/CustomLinkChannel.svelte';

  export let workspace: ProjectWorkspace;
  export let channel: WorkbenchChannel;
  export let onInspect: (item: InspectorItem) => void;
</script>

{#if channel.type === 'overview'}
  <OverviewChannel {workspace} {onInspect} />
{:else if channel.type === 'inbox'}
  <InboxChannel messages={workspace.inbox} {onInspect} />
{:else if channel.type === 'tasks'}
  <TasksChannel tasks={workspace.tasks} {onInspect} />
{:else if channel.type === 'ai_sessions'}
  <AISessionsChannel sessions={workspace.aiSessions} {onInspect} />
{:else if channel.type === 'notes_sources'}
  <NotesSourcesChannel items={workspace.notesSources} {onInspect} />
{:else if channel.type === 'cron'}
  <CronChannel plans={workspace.cronPlans} {onInspect} />
{:else}
  <CustomLinkChannel {channel} />
{/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:

<script lang="ts">
  import type { InspectorItem, WorkTask } from '../types';
  export let tasks: WorkTask[];
  export let onInspect: (item: InspectorItem) => void;

  function inspectTask(task: WorkTask) {
    onInspect({
      title: task.title,
      type: 'Task',
      description: task.summary,
      properties: [
        { label: 'Status', value: task.completed ? 'Completed' : 'Open' },
        { label: 'Owner', value: task.owner },
        { label: 'Due', value: task.due },
        { label: 'Tag', value: task.tag },
      ],
    });
  }
</script>

<section class="channel-page">
  <header class="channel-header">
    <p>Work Plan</p>
    <h1>Tasks</h1>
  </header>
  <div class="task-list">
    {#each tasks as task}
      <article class:completed={task.completed} class="task-card">
        <span class="task-check" aria-hidden="true">{task.completed ? '鉁? : '?}</span>
        <div>
          <h2>{task.title}</h2>
          <p>{task.summary}</p>
          <small>{task.owner}{task.due} 路 #{task.tag}</small>
        </div>
        <button aria-label={`Inspect ${task.title}`} on:click={() => inspectTask(task)}>Inspect</button>
      </article>
    {/each}
  </div>
</section>

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:

<script lang="ts">
  import type { InspectorItem } from './types';
  export let item: InspectorItem | null;
  let activeTab: 'discussion' | 'properties' | 'more' = 'discussion';
</script>

<aside class="object-inspector" aria-label="Object inspector">
  <div class="inspector-tabs" role="tablist" aria-label="Inspector tabs">
    <button aria-selected={activeTab === 'discussion'} on:click={() => (activeTab = 'discussion')}>Discussion</button>
    <button aria-selected={activeTab === 'properties'} on:click={() => (activeTab = 'properties')}>Properties</button>
    <button aria-selected={activeTab === 'more'} on:click={() => (activeTab = 'more')}>More</button>
  </div>

  {#if item}
    <h2>{item.title}</h2>
    <p>{item.description}</p>
    {#if activeTab === 'discussion'}
      <p>No comments yet. Add project discussion here later.</p>
    {:else if activeTab === 'properties'}
      <dl>
        {#each item.properties as property}
          <div>
            <dt>{property.label}</dt>
            <dd>{property.value}</dd>
          </div>
        {/each}
      </dl>
    {:else}
      <button type="button">Copy link</button>
      <button type="button">Archive</button>
    {/if}
  {:else}
    <h2>Inspector</h2>
    <p>Select an item to inspect discussion, properties, and actions.</p>
  {/if}
</aside>

Update ProjectWorkbench.svelte so ChannelContent receives the selected channel and ObjectInspector receives selected item.

  • Step 6: Run channel tests

Run:

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:

Set-Location D:\work\senlinai\agent
git add apps\web\src\features\workbench
git commit -m "feat: add workbench channel templates"