Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.6"
},
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/__tests__/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('Middleware Route Protection', () => {
expect(response.headers.get('location')).toBeNull()
})

it('redirects authenticated from /login to /dashboard', async () => {
it('redirects authenticated from /login to /tasks', async () => {
const mockUser = {
id: 'user-123',
email: 'test@example.com',
Expand All @@ -124,7 +124,7 @@ describe('Middleware Route Protection', () => {

expect(response.status).toBe(307)
expect(response.headers.get('location')).toBe(
'http://localhost:3000/dashboard'
'http://localhost:3000/tasks'
)
})
})
Expand Down Expand Up @@ -249,7 +249,7 @@ describe('Middleware Route Protection', () => {
expect(response.headers.get('location')).toBeNull()
})

it('redirects authenticated from /signup to /dashboard', async () => {
it('redirects authenticated from /signup to /tasks', async () => {
const mockUser = {
id: 'user-123',
email: 'test@example.com',
Expand All @@ -264,7 +264,7 @@ describe('Middleware Route Protection', () => {

expect(response.status).toBe(307)
expect(response.headers.get('location')).toBe(
'http://localhost:3000/dashboard'
'http://localhost:3000/tasks'
)
})
})
Expand Down
70 changes: 70 additions & 0 deletions apps/web/src/app/(dashboard)/dashboard-client-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client'

import { useState, createContext, useContext, useCallback } from 'react'
import { DashboardLayout } from '@/components/templates/DashboardLayout'

interface DashboardContextValue {
/** Set content to override the right panel (replaces feed), or null to restore feed */
setRightPanelOverride: (content: React.ReactNode | null) => void
}

const DashboardContext = createContext<DashboardContextValue>({
setRightPanelOverride: () => {},
})

/**
* Hook to access the dashboard context for controlling the right panel content.
* Use `setRightPanelOverride` to replace the activity feed with custom content
* (e.g., TaskDetailInlinePanel), or pass null to restore the feed.
*/
export function useDashboardContext() {
return useContext(DashboardContext)
}

interface DashboardClientWrapperProps {
/** Header content for the dashboard */
header: React.ReactNode
/** Sidebar content (e.g., AgentSidebarWithPanel) */
sidebar: React.ReactNode
/** Default right panel content (e.g., LiveFeed) */
feed: React.ReactNode
/** Main content area */
children: React.ReactNode
/** Additional CSS classes */
className?: string
}

/**
* DashboardClientWrapper is a client component that wraps DashboardLayout
* and provides context for child components to swap the right panel content.
*
* This bridges the gap between the server-side DashboardShell (which fetches data)
* and client-side components (like TasksClient) that need to control the layout.
*/
export function DashboardClientWrapper({
header,
sidebar,
feed,
children,
className,
}: DashboardClientWrapperProps) {
const [rightPanelOverride, setRightPanelOverrideState] = useState<React.ReactNode | null>(null)

const setRightPanelOverride = useCallback((content: React.ReactNode | null) => {
setRightPanelOverrideState(content)
}, [])

return (
<DashboardContext.Provider value={{ setRightPanelOverride }}>
<DashboardLayout
header={header}
sidebar={sidebar}
feed={feed}
rightPanelOverride={rightPanelOverride}
className={className}
>
{children}
</DashboardLayout>
</DashboardContext.Provider>
)
}
51 changes: 45 additions & 6 deletions apps/web/src/app/(dashboard)/dashboard-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { DashboardLayout } from '@/components/templates/DashboardLayout'
import { DashboardClientWrapper } from './dashboard-client-wrapper'
import { Header } from '@/components/organisms/Header'
import {
AgentSidebarWithPanel,
type AgentSidebarWithPanelProps,
} from '@/components/organisms/AgentSidebarWithPanel'
import { type AgentData } from '@/components/organisms/AgentSidebar'
import { LiveFeed, type ActivityData } from '@/components/organisms/LiveFeed'
import type { AgentProfileData } from '@/components/templates'

interface DashboardShellProps {
children: React.ReactNode
Expand Down Expand Up @@ -44,6 +45,7 @@ export async function DashboardShell({ children }: DashboardShellProps) {

// Initialize empty data structures
let agents: AgentData[] = []
let agentProfiles = new Map<string, AgentProfileData>()
let activities: ActivityData[] = []
let pendingTasksCount = 0

Expand All @@ -60,8 +62,14 @@ export async function DashboardShell({ children }: DashboardShellProps) {
status,
blocked_reason,
current_task_id,
status_reason,
status_since,
agent_specs!inner (
avatar_color
avatar_color,
description,
personality,
expertise,
collaborates_with
),
tasks:current_task_id (
title
Expand All @@ -84,6 +92,35 @@ export async function DashboardShell({ children }: DashboardShellProps) {
: null,
blockedReason: agent.blocked_reason,
}))

// Build extended agent profiles map for the sidebar panel
agentProfiles = new Map<string, AgentProfileData>()
for (const agent of agentsData) {
const specs = agent.agent_specs as {
avatar_color?: string | null
description?: string | null
personality?: string | null
expertise?: string[] | null
collaborates_with?: string[] | null
}
agentProfiles.set(agent.id, {
id: agent.id,
name: agent.name,
role: agent.role,
status: agent.status as AgentProfileData['status'],
avatarColor: specs?.avatar_color ?? undefined,
currentTask: agent.tasks
? (agent.tasks as { title?: string })?.title
: null,
blockedReason: agent.blocked_reason,
description: specs?.description ?? null,
personality: specs?.personality ?? null,
expertise: specs?.expertise ?? null,
collaborates_with: specs?.collaborates_with ?? null,
statusReason: agent.status_reason ?? null,
statusSince: agent.status_since ?? null,
})
}
}

// Fetch recent activities with agent names
Expand Down Expand Up @@ -148,19 +185,21 @@ export async function DashboardShell({ children }: DashboardShellProps) {
const totalAgentsCount = agents.length

return (
<DashboardLayout
<DashboardClientWrapper
header={
<Header
squadName={squads?.[0]?.name}
squadStatus="active"
activeAgentsCount={activeAgentsCount}
totalAgentsCount={totalAgentsCount}
pendingTasksCount={pendingTasksCount}
docsUrl="https://docs.missioncontrol.dev"
isConnected={true}
/>
}
sidebar={<AgentSidebarWithPanel agents={agents} />}
sidebar={<AgentSidebarWithPanel agents={agents} agentProfiles={agentProfiles} />}
feed={<LiveFeed activities={activities} maxItems={15} />}
>
{children}
</DashboardLayout>
</DashboardClientWrapper>
)
}
94 changes: 2 additions & 92 deletions apps/web/src/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,5 @@
import Link from 'next/link'
import { redirect } from 'next/navigation'
import { Plus } from 'lucide-react'
import { createClient } from '@/lib/supabase/server'
import { Text, Button } from '@/components/atoms'

export default async function DashboardPage() {
const supabase = await createClient()

// Get current user
const {
data: { user },
} = await supabase.auth.getUser()

if (!user) {
redirect('/login')
}

// Fetch user's squads - RLS automatically filters to only their squads
const { data: squads, error } = await supabase
.from('squads')
.select('id, name, description, created_at')
.order('created_at', { ascending: false })

return (
<div className="space-y-6">
<div>
<Text variant="heading" as="h1" className="text-2xl">
Dashboard
</Text>
<Text variant="body" className="mt-1 text-text-secondary">
Welcome back, {user.email}
</Text>
</div>

<div className="space-y-4">
<Text variant="heading" as="h2" className="text-lg">
Your Squads
</Text>

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-950 dark:text-red-300">
Error loading squads: {error.message}
</div>
)}

{!error && squads?.length === 0 && (
<div className="rounded-lg border border-border bg-background-card p-6 text-center">
<Text variant="body" className="text-text-secondary">
You don&apos;t have any squads yet.
</Text>
<Text variant="caption" className="mt-2 text-text-muted">
Create your first squad to get started with AI agent coordination.
</Text>
<Link href="/onboarding" className="mt-4 inline-block">
<Button>
<Plus className="mr-2 h-4 w-4" />
Create Squad
</Button>
</Link>
</div>
)}

{!error && squads && squads.length > 0 && (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{squads.map((squad) => (
<Link
key={squad.id}
href={`/squads/${squad.id}`}
className="block rounded-lg border border-border bg-background-card p-4 transition-colors hover:bg-background-elevated"
>
<Text variant="heading" as="h3" className="text-base">
{squad.name}
</Text>
{squad.description && (
<Text variant="body" className="mt-1 text-sm text-text-secondary">
{squad.description}
</Text>
)}
<Text variant="caption" className="mt-2 text-text-muted">
Created{' '}
{new Date(squad.created_at!).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</Text>
</Link>
))}
</div>
)}
</div>
</div>
)
export default function DashboardPage() {
redirect('/tasks')
}
10 changes: 0 additions & 10 deletions apps/web/src/app/(dashboard)/tasks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,6 @@ export default async function TasksPage() {

return (
<div className="space-y-6">
<div>
<Text variant="heading" as="h1" className="text-2xl">
Tasks
</Text>
<Text variant="body" className="mt-1 text-text-secondary">
{transformedTasks.length} {transformedTasks.length === 1 ? 'task' : 'tasks'} across your
squads
</Text>
</div>

{error && (
<div className="rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-950 dark:text-red-300">
Error loading tasks: {error.message}
Expand Down
Loading
Loading