superpowers
This commit is contained in:
232
.superpowers/sdd/task-4-brief.md
Normal file
232
.superpowers/sdd/task-4-brief.md
Normal file
@@ -0,0 +1,232 @@
|
||||
## 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user