24 KiB
Review package Task 4 v2
Commits
1a82a8b fix: address workbench inspector review findings
3194cbe feat: add workbench channel templates
Stat
.superpowers/sdd/task-4-report.md | 67 ++++++++++++++++++++++ .../src/features/workbench/ChannelContent.svelte | 30 ++++++++++ .../src/features/workbench/ChannelContent.test.ts | 39 +++++++++++++ .../src/features/workbench/ObjectInspector.svelte | 37 ++++++++++++ .../src/features/workbench/ProjectWorkbench.svelte | 15 +++-- .../features/workbench/ProjectWorkbench.test.ts | 22 ++++++- .../workbench/channels/AISessionsChannel.svelte | 27 +++++++++ .../features/workbench/channels/CronChannel.svelte | 19 ++++++ .../workbench/channels/CustomLinkChannel.svelte | 16 ++++++ .../workbench/channels/InboxChannel.svelte | 22 +++++++ .../workbench/channels/NotesSourcesChannel.svelte | 19 ++++++ .../workbench/channels/OverviewChannel.svelte | 29 ++++++++++ .../workbench/channels/TasksChannel.svelte | 23 ++++++++ 13 files changed, 360 insertions(+), 5 deletions(-)
Diff
diff --git a/.superpowers/sdd/task-4-report.md b/.superpowers/sdd/task-4-report.md
new file mode 100644
index 0000000..b743d8d
--- /dev/null
+++ b/.superpowers/sdd/task-4-report.md
@@ -0,0 +1,67 @@
+# Task 4 Report: Channel Content Templates And Object Inspector
+
+## Status
+
+Implemented and committed the Task 4 workbench channel templates and object inspector.
+
+## What Changed
+
+- Added ChannelContent.svelte, which dispatches ProjectWorkspace data to the selected system or custom channel template.
+- Added seven channel templates for overview, inbox, tasks, AI sessions, notes and sources, cron plans, and external custom links.
+- Made all applicable records selectable through an Inspect <title> action that supplies an InspectorItem to the workbench.
+- Added ObjectInspector.svelte with Discussion, Properties, and More tabs, including empty-state behavior and object properties.
+- Wired the selected channel and selected inspector item into ProjectWorkbench.svelte.
+- Added the required channel dispatcher test, including the task-to-inspector callback assertion.
+- Kept visual-system CSS untouched for Task 5.
+
+## TDD Evidence
+
+1. Added apps/web/src/features/workbench/ChannelContent.test.ts before creating any production channel component.
+2. Ran npm test -- --run src/features/workbench/ChannelContent.test.ts from apps/web.
+3. Observed the expected red failure: Vite could not resolve ./ChannelContent.svelte because it did not exist.
+4. Implemented the dispatcher and channel components, then ran the focused channel and workbench tests.
+5. Corrected the inspector tab semantics after the green run exposed Svelte accessibility warnings, then reran verification with no warnings.
+
+## Tests
+
+- npm test -- --run src/features/workbench/ChannelContent.test.ts src/features/workbench/ProjectWorkbench.test.ts
-
- Passed: 2 test files, 4 tests.
+-
npm run build
- Passed: 2 test files, 4 tests.
+-
-
- Passed: Vite production build completed successfully.
+-
git diff --check
- Passed: Vite production build completed successfully.
+-
-
- Passed: no whitespace errors.
+## Files Changed
+
+- Created apps/web/src/features/workbench/ChannelContent.svelte
+- Created apps/web/src/features/workbench/ChannelContent.test.ts
+- Created apps/web/src/features/workbench/ObjectInspector.svelte
+- Created apps/web/src/features/workbench/channels/OverviewChannel.svelte
+- Created apps/web/src/features/workbench/channels/InboxChannel.svelte
+- Created apps/web/src/features/workbench/channels/TasksChannel.svelte
+- Created apps/web/src/features/workbench/channels/AISessionsChannel.svelte
+- Created apps/web/src/features/workbench/channels/NotesSourcesChannel.svelte
+- Created apps/web/src/features/workbench/channels/CronChannel.svelte
+- Created apps/web/src/features/workbench/channels/CustomLinkChannel.svelte
+- Modified apps/web/src/features/workbench/ProjectWorkbench.svelte
+- Created this report: .superpowers/sdd/task-4-report.md
+
+## Self-Review
+
+- Confirmed every WorkbenchChannel type has a dedicated rendered template through the dispatcher.
+- Confirmed the custom link uses the required Open external channel text and provides a copy-style action.
+- Confirmed task inspection produces the exact task title required by the test.
+- Confirmed the inspector remains visible through its existing aria-label and exposes semantic tab state using role="tab" with aria-selected.
+- Confirmed no React, shadcn/ui dependency, or index.css modification was introduced.
+- Confirmed unrelated existing .superpowers/sdd files were not staged.
+
+## Concerns
+
+- The copy URL and inspector More-tab actions are present as MVP UI controls; beyond writing the custom URL to the clipboard, they intentionally do not persist or mutate workbench data.
+- Visual styling is limited to semantic class hooks by design; Task 5 owns the visual-system CSS work.
+
+## Review Fixes
+
+- Replaced the incomplete ARIA tabs pattern in ObjectInspector.svelte with ordinary pressed view-switcher buttons, removing tab and tablist roles that required a full tab-panel keyboard interaction model.
+- Extended ProjectWorkbench.test.ts to select the Work Plan channel, inspect a task, and assert the rendered inspector title plus Properties values within the object inspector.
+- Focused workbench coverage passed after the changes: 2 test files, 5 tests.
diff --git a/apps/web/src/features/workbench/ChannelContent.svelte b/apps/web/src/features/workbench/ChannelContent.svelte
new file mode 100644
index 0000000..9e81962
--- /dev/null
+++ b/apps/web/src/features/workbench/ChannelContent.svelte
@@ -0,0 +1,30 @@
+<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} diff --git a/apps/web/src/features/workbench/ChannelContent.test.ts b/apps/web/src/features/workbench/ChannelContent.test.ts new file mode 100644 index 0000000..41c4368 --- /dev/null +++ b/apps/web/src/features/workbench/ChannelContent.test.ts @@ -0,0 +1,39 @@ +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');
- }); +}); diff --git a/apps/web/src/features/workbench/ObjectInspector.svelte b/apps/web/src/features/workbench/ObjectInspector.svelte new file mode 100644 index 0000000..79fa781 --- /dev/null +++ b/apps/web/src/features/workbench/ObjectInspector.svelte @@ -0,0 +1,37 @@ +<script lang="ts">
- import type { InspectorItem } from './types';
- export let item: InspectorItem | null;
- let activeTab: 'discussion' | 'properties' | 'more' = 'discussion'; +</script>
+
diff --git a/apps/web/src/features/workbench/ProjectWorkbench.svelte b/apps/web/src/features/workbench/ProjectWorkbench.svelte index 4ab9ffd..3d29c30 100644 --- a/apps/web/src/features/workbench/ProjectWorkbench.svelte +++ b/apps/web/src/features/workbench/ProjectWorkbench.svelte @@ -1,41 +1,48 @@ <script lang="ts"> import { getProjectWorkspace, workbenchProjects } from './mockData'; + import ChannelContent from './ChannelContent.svelte'; + import ObjectInspector from './ObjectInspector.svelte'; import ProjectRail from './ProjectRail.svelte'; import ProjectChannelSidebar from './ProjectChannelSidebar.svelte'; import WorkspaceTopbar from './WorkspaceTopbar.svelte'; + import type { InspectorItem } from './types'; export let currentUser: { account: string }; let selectedProjectID = workbenchProjects[0].id; + let selectedItem: InspectorItem | null = null; $: workspace = getProjectWorkspace(selectedProjectID); $: selectedChannelID = workspace.channels[0].id; + $: selectedChannel = workspace.channels.find((channel) => channel.id === selectedChannelID) ?? workspace.channels[0]; function selectProject(projectID: number) { selectedProjectID = projectID; } function selectChannel(channelID: string) { selectedChannelID = channelID; } + + function inspectItem(item: InspectorItem) { + selectedItem = item; + } </script>{workspace.channels.find((channel) => channel.id === selectedChannelID)?.title}
+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();
@@ -16,11 +16,31 @@ describe('ProjectWorkbench', () => { });
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();
}); +
- it('shows inspected task details in the inspector', async () => {
- render(ProjectWorkbench, { props: { currentUser: { account: 'david@example.com' } } });
- const inspector = screen.getByLabelText('Object inspector');
- expect(within(inspector).getByRole('button', { name: 'Discussion' })).toHaveAttribute('aria-pressed', 'true');
- await fireEvent.click(screen.getByRole('button', { name: 'Work Plan 8' }));
- await fireEvent.click(screen.getByRole('button', { name: 'Inspect Confirm homepage information architecture' }));
- expect(within(inspector).getByRole('heading', { name: 'Confirm homepage information architecture' })).toBeInTheDocument();
- await fireEvent.click(within(inspector).getByRole('button', { name: 'Properties' }));
- expect(within(inspector).getByRole('button', { name: 'Properties' })).toHaveAttribute('aria-pressed', 'true');
- expect(within(inspector).getByText('Status')).toBeInTheDocument();
- expect(within(inspector).getByText('Open')).toBeInTheDocument();
- expect(within(inspector).getByText('Owner')).toBeInTheDocument();
- expect(within(inspector).getByText('David')).toBeInTheDocument();
- }); }); diff --git a/apps/web/src/features/workbench/channels/AISessionsChannel.svelte b/apps/web/src/features/workbench/channels/AISessionsChannel.svelte new file mode 100644 index 0000000..3b52383 --- /dev/null +++ b/apps/web/src/features/workbench/channels/AISessionsChannel.svelte @@ -0,0 +1,27 @@ +<script lang="ts">
- import type { AISessionItem, InspectorItem } from '../types';
- export let sessions: AISessionItem[];
- export let onInspect: (item: InspectorItem) => void;
- let selectedSessionID = sessions[0]?.id;
- $: selectedSession = sessions.find((session) => session.id === selectedSessionID) ?? sessions[0];
- function inspectSession(session: AISessionItem) {
- onInspect({ title: session.title, type: 'AI session', description: session.summary, properties: [{ label: 'Updated', value: session.updatedAt }, { label: 'References', value: session.references.join(', ') }] });
- } +</script>
+
-
Project intelligence
AI Sessions
-
-
-
{#each sessions as session} -
<button class:active={selectedSession?.id === session.id} on:click={() => (selectedSessionID = session.id)}>{session.title}<small>{session.updatedAt}</small></button> -
{/each} - {#if selectedSession}
-
<article class="conversation-summary"><h2>{selectedSession.title}</h2><p>{selectedSession.summary}</p><p>References: {selectedSession.references.join(', ')}</p><button aria-label={`Inspect ${selectedSession.title}`} on:click={() => inspectSession(selectedSession)}>Inspect</button></article> - {/if}
+
- import type { CronPlan, InspectorItem } from '../types';
- export let plans: CronPlan[];
- export let onInspect: (item: InspectorItem) => void;
- function inspectPlan(plan: CronPlan) {
- onInspect({ title: plan.title, type: 'Cron plan', description:
Scheduled by ${plan.owner}., properties: [{ label: 'State', value: plan.enabled ? 'Enabled' : 'Disabled' }, { label: 'Schedule', value: plan.schedule }, { label: 'Next run', value: plan.nextRun }, { label: 'Last result', value: plan.lastResult }] }); - } +</script>
+
-
Scheduled work
Cron Plans
-
- {#each plans as plan}
-
<article class="cron-row"><div><h2>{plan.title}</h2><p>{plan.schedule} · {plan.nextRun}</p><small>{plan.enabled ? 'Enabled' : 'Disabled'} · {plan.lastResult}</small></div><button aria-label={`Inspect ${plan.title}`} on:click={() => inspectPlan(plan)}>Inspect</button></article> - {/each}
+
- import type { WorkbenchChannel } from '../types';
- export let channel: WorkbenchChannel;
- function copyUrl() {
- void navigator.clipboard?.writeText(channel.url ?? '');
- } +</script>
+
-
External resource
{channel.title}
-
{channel.url}
- Open external channel
- Copy URL +
+
-
Message Flow
Inbox
-
- {#each messages as message}
-
<article class="message-row"> -
<div><small>{message.source} · {message.time}</small><h2>{message.title}</h2><p>{message.summary}</p><span>{message.status} · #{message.tag}</span></div> -
<button aria-label={`Inspect ${message.title}`} on:click={() => inspectMessage(message)}>Inspect</button> -
</article> - {/each}
+
- import type { InspectorItem, NoteSourceItem } from '../types';
- export let items: NoteSourceItem[];
- export let onInspect: (item: InspectorItem) => void;
- function inspectItem(item: NoteSourceItem) {
- onInspect({ title: item.title, type: item.kind, description:
${item.source} record in this project., properties: [{ label: 'Updated', value: item.updatedAt }, { label: 'Tag', value: item.tag }, { label: 'Source', value: item.source }] }); - } +</script>
+
-
Project reference
Notes & Sources
-
- {#each items as item}
-
<article class="file-row"><span>{item.kind}</span><div><h2>{item.title}</h2><small>{item.source} · {item.updatedAt} · #{item.tag}</small></div><button aria-label={`Inspect ${item.title}`} on:click={() => inspectItem(item)}>Inspect</button></article> - {/each}
+
- import type { InspectorItem, ProjectWorkspace } from '../types';
- export let workspace: ProjectWorkspace;
- export let onInspect: (item: InspectorItem) => void;
- const metrics = (workspace: ProjectWorkspace) => [
- { label: 'Inbox', value: workspace.inbox.length, description: 'Messages waiting for review' },
- { label: 'Tasks', value: workspace.tasks.length, description: 'Work plan records' },
- { label: 'AI Sessions', value: workspace.aiSessions.length, description: 'Saved conversations' },
- { label: 'Notes & Sources', value: workspace.notesSources.length, description: 'Project reference records' },
- { label: 'Cron Plans', value: workspace.cronPlans.length, description: 'Scheduled task records' },
- ]; +</script>
+
-
{workspace.project.name}
-
Overview
-
- {#each metrics(workspace) as metric}
-
<button class="metric-card" on:click={() => onInspect({ title: metric.label, type: 'Workspace metric', description: metric.description, properties: [{ label: 'Records', value: String(metric.value) }] })}> -
<strong>{metric.value}</strong> -
<span>{metric.label}</span> -
</button> - {/each}
+
- 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>
+
-
Work Plan
Tasks
-
- {#each tasks as task}
-
<article class:completed={task.completed} class="task-card"> -
<span class="task-check" aria-hidden="true">{task.completed ? 'Done' : 'Open'}</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}
+