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.sveltedispatcher
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 withOpen external channellink and copy-style button. -
Step 5: Implement
ObjectInspector.svelteand wire it intoProjectWorkbench.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"