## 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
<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`:

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

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

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

---

