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>
+
+
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.completed ? 'Done' : 'Open'}
+ {task.title}
{task.summary}
{task.owner} · {task.due} · #{task.tag}
+ inspectTask(task)}>Inspect
+
+ {/each}
+
+