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

18 KiB

Review package Task 4

Commits

3194cbe feat: add workbench channel templates

Stat

.../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 ++++++--- .../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 +++++++++++++ 11 files changed, 272 insertions(+), 4 deletions(-)

Diff

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..f36f387 --- /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}

+
- +
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} + (selectedSessionID = session.id)}>{session.title}{session.updatedAt} + {/each} +
+ {#if selectedSession} +

{selectedSession.title}

{selectedSession.summary}

References: {selectedSession.references.join(', ')}

inspectSession(selectedSession)}>Inspect
+ {/if} +
+
diff --git a/apps/web/src/features/workbench/channels/CronChannel.svelte b/apps/web/src/features/workbench/channels/CronChannel.svelte new file mode 100644 index 0000000..7a6f2c3 --- /dev/null +++ b/apps/web/src/features/workbench/channels/CronChannel.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + 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} +

{plan.title}

{plan.schedule} · {plan.nextRun}

{plan.enabled ? 'Enabled' : 'Disabled'} · {plan.lastResult}
inspectPlan(plan)}>Inspect
+ {/each} +
+
diff --git a/apps/web/src/features/workbench/channels/CustomLinkChannel.svelte b/apps/web/src/features/workbench/channels/CustomLinkChannel.svelte new file mode 100644 index 0000000..0fa9111 --- /dev/null +++ b/apps/web/src/features/workbench/channels/CustomLinkChannel.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + 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 +
diff --git a/apps/web/src/features/workbench/channels/InboxChannel.svelte b/apps/web/src/features/workbench/channels/InboxChannel.svelte new file mode 100644 index 0000000..2d25d63 --- /dev/null +++ b/apps/web/src/features/workbench/channels/InboxChannel.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import type { InboxMessage, InspectorItem } from '../types'; + + export let messages: InboxMessage[]; + export let onInspect: (item: InspectorItem) => void; + + function inspectMessage(message: InboxMessage) { + onInspect({ title: message.title, type: 'Inbox message', description: message.summary, properties: [{ label: 'Source', value: message.source }, { label: 'Status', value: message.status }, { label: 'Tag', value: message.tag }, { label: 'Time', value: message.time }] }); + } +</script> + +
+

Message Flow

Inbox

+
+ {#each messages as message} +
+
{message.source} · {message.time}

{message.title}

{message.summary}

{message.status} · #{message.tag}
+ inspectMessage(message)}>Inspect +
+ {/each} +
+
diff --git a/apps/web/src/features/workbench/channels/NotesSourcesChannel.svelte b/apps/web/src/features/workbench/channels/NotesSourcesChannel.svelte new file mode 100644 index 0000000..256ef8b --- /dev/null +++ b/apps/web/src/features/workbench/channels/NotesSourcesChannel.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + 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} +
{item.kind}

{item.title}

{item.source} · {item.updatedAt} · #{item.tag}
inspectItem(item)}>Inspect
+ {/each} +
+
diff --git a/apps/web/src/features/workbench/channels/OverviewChannel.svelte b/apps/web/src/features/workbench/channels/OverviewChannel.svelte new file mode 100644 index 0000000..6d478cf --- /dev/null +++ b/apps/web/src/features/workbench/channels/OverviewChannel.svelte @@ -0,0 +1,29 @@ +<script lang="ts"> + 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} + onInspect({ title: metric.label, type: 'Workspace metric', description: metric.description, properties: [{ label: 'Records', value: String(metric.value) }] })}> + {metric.value} + {metric.label} + + {/each} +
+
diff --git a/apps/web/src/features/workbench/channels/TasksChannel.svelte b/apps/web/src/features/workbench/channels/TasksChannel.svelte new file mode 100644 index 0000000..b59e47e --- /dev/null +++ b/apps/web/src/features/workbench/channels/TasksChannel.svelte @@ -0,0 +1,23 @@ +<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> + +
+

Work Plan

Tasks

+
+ {#each tasks as task} +
+ +

{task.title}

{task.summary}

{task.owner} · {task.due} · #{task.tag}
+ inspectTask(task)}>Inspect +
+ {/each} +
+