feat(ux): consolidate Launch Swarm + telemetry UX with minimized strip

- Removed broken LaunchSwarmDialog (formula-based) from TopBar/LeftPanel
- All Rocket buttons (TopBar, LeftPanel, DAG nodes, social cards) now open
  AssignmentPanel (archetype-based) which actually works
- Every Rocket clears taskId first so assignMode && !taskId condition passes
- Conversation button priority: taskId always shows conversation, not assign panel
- Added TelemetryStrip: minimized right sidebar with status dots when non-telemetry
  panel (conversation/assignment) is active
- Live feed has minimize button → restores last taskId or assignMode
- DAG nodes: Signal icon → restores telemetry feed
- Social button on DAG nodes: single router.push to avoid race (setView + setTaskId)
- Fixed social card message button: opens right panel with drawer:closed (no popup)

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
zenchantlive 2026-03-01 18:17:58 -08:00
parent 65d69ecbbc
commit c246ceaf21
165 changed files with 13730 additions and 1132 deletions

View file

@ -0,0 +1,37 @@
import { NextResponse } from 'next/server';
import { runBdCommand } from '../../../../lib/bridge';
export const dynamic = 'force-dynamic';
export async function GET(request: Request): Promise<Response> {
const { searchParams } = new URL(request.url);
const projectRoot = searchParams.get('projectRoot') ?? process.cwd();
const result = await runBdCommand({
projectRoot,
args: ['--version'],
timeoutMs: 8_000,
});
if (!result.success) {
const status = result.classification === 'not_found' ? 503 : 500;
return NextResponse.json(
{
ok: false,
error: {
classification: result.classification ?? 'unknown',
message: result.error ?? result.stderr ?? 'bd health check failed',
},
},
{ status },
);
}
return NextResponse.json({
ok: true,
data: {
version: result.stdout,
},
});
}

View file

@ -29,7 +29,7 @@ export async function handleMutationRequest(request: Request, operation: Mutatio
const payload = validateMutationPayload(operation, body);
const result = await executeMutation(operation, payload);
const status = result.ok ? 200 : result.error?.classification === 'not_found' ? 404 : 400;
const status = result.ok ? 200 : result.error?.classification === 'not_found' ? 503 : 400;
return NextResponse.json(result, { status });
} catch (error) {
if (error instanceof MutationValidationError) {

View file

@ -0,0 +1,56 @@
import { NextResponse } from 'next/server';
import { writeCoordEvent } from '../../../../lib/coord-events';
interface CoordEventsDeps {
writeCoordEvent: typeof writeCoordEvent;
}
function parseBody(data: unknown): { projectRoot: string; event: unknown } | null {
if (!data || typeof data !== 'object') return null;
const record = data as Record<string, unknown>;
if (typeof record.projectRoot !== 'string' || !record.projectRoot.trim()) return null;
return {
projectRoot: record.projectRoot.trim(),
event: record.event,
};
}
export async function handleCoordEventsPost(
request: Request,
deps?: Partial<CoordEventsDeps>,
): Promise<Response> {
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'Invalid JSON body' } },
{ status: 400 },
);
}
const parsed = parseBody(body);
if (!parsed) {
return NextResponse.json(
{ ok: false, error: { classification: 'bad_args', message: 'projectRoot and event are required' } },
{ status: 400 },
);
}
const writer = deps?.writeCoordEvent ?? writeCoordEvent;
const result = await writer(parsed.event, { projectRoot: parsed.projectRoot });
if (!result.ok) {
const status = result.error.classification === 'bad_args' ? 400 : 500;
return NextResponse.json(
{ ok: false, error: { classification: result.error.classification, message: result.error.message } },
{ status },
);
}
return NextResponse.json({ ok: true, eventId: result.eventId });
}
export async function POST(request: Request): Promise<Response> {
return handleCoordEventsPost(request);
}

View file

@ -22,7 +22,7 @@ export async function GET(
const activity = history.filter((e: ActivityEvent) => e.beadId === beadId);
// 2. Get communication for this bead
const summary = await getCommunicationSummary();
const summary = await getCommunicationSummary(projectRoot);
const messages = summary.messages.filter((m: AgentMessage) => m.bead_id === beadId);
// 3. Get local bd interactions via CLI
@ -55,4 +55,4 @@ export async function GET(
console.error('[API/Sessions/Conversation] Failed:', error);
return NextResponse.json({ ok: false, error: String(error) }, { status: 500 });
}
}
}

View file

@ -41,9 +41,9 @@ export async function GET(request: Request): Promise<Response> {
try {
const issues = await readIssuesFromDisk({ projectRoot, preferBd: true });
const activity = activityEventBus.getHistory(projectRoot);
const communication = await getCommunicationSummary();
const communication = await getCommunicationSummary(projectRoot);
const livenessMap = await getAgentLivenessMap(projectRoot, activity);
const incursions = await calculateIncursions();
const incursions = await calculateIncursions(projectRoot, livenessMap);
const agentsResult = await listAgents({}, { projectRoot });
const feed = buildSessionTaskFeed(issues, activity, communication, livenessMap);

View file

@ -21,9 +21,10 @@ export async function GET(request: Request): Promise<Response> {
});
if (!result.success) {
const status = result.classification === 'not_found' ? 503 : 400;
return NextResponse.json(
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
{ status: 400 },
{ status },
);
}

View file

@ -29,9 +29,10 @@ export async function GET(request: Request): Promise<Response> {
});
if (!result.success) {
const status = result.classification === 'not_found' ? 503 : 400;
return NextResponse.json(
{ ok: false, error: { classification: result.classification ?? 'unknown', message: result.error ?? result.stderr } },
{ status: 400 },
{ status },
);
}

View file

@ -6,27 +6,27 @@
:root {
/* ========== VISUAL-TRUTH UI TOKEN CONTRACT (bb-vt.1.1) ========== */
/* Warm charcoal aurora palette - DISTINCT LAYERS */
--ui-bg-app: #181716; /* Darkest - page background */
--ui-bg-header: #131211; /* Header - darker than sidebar */
--ui-bg-shell: #1f1e1d; /* Sidebar - distinct from main */
--ui-bg-panel: #282725; /* Panels/cards within sidebar */
--ui-bg-main: #242322; /* Main content area - distinct */
--ui-bg-card: #302e2c; /* Cards - lightest layer */
--ui-bg-elevated: #3a3836; /* Elevated/selected elements */
/* Map legacy --ui-* tokens to theme tokens so all themes work consistently */
--ui-bg-app: var(--surface-backdrop);
--ui-bg-header: var(--surface-primary);
--ui-bg-shell: var(--surface-primary);
--ui-bg-panel: var(--surface-secondary);
--ui-bg-main: var(--surface-secondary);
--ui-bg-card: var(--surface-elevated);
--ui-bg-elevated: var(--surface-elevated);
--ui-border-soft: rgba(180, 175, 165, 0.2);
--ui-border-strong: rgba(180, 175, 165, 0.35);
--ui-border-soft: var(--border-subtle);
--ui-border-strong: var(--border-default);
--ui-text-primary: #f0eeea;
--ui-text-muted: #a8a49a;
--ui-text-primary: var(--text-primary);
--ui-text-muted: var(--text-tertiary);
--ui-accent-ready: #35d98f;
--ui-accent-blocked: #ff4c72;
--ui-accent-warning: #ffb24a;
--ui-accent-info: #35c9ff;
--ui-accent-action-green: #35d98f;
--ui-accent-action-red: #ff4c72;
--ui-accent-ready: var(--accent-success);
--ui-accent-blocked: var(--accent-danger);
--ui-accent-warning: var(--accent-warning);
--ui-accent-info: var(--accent-info);
--ui-accent-action-green: var(--accent-success);
--ui-accent-action-red: var(--accent-danger);
--ui-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
--ui-font-mono: 'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
@ -43,30 +43,25 @@
--color-accent-teal: var(--ui-accent-info);
--color-text-primary: var(--ui-text-primary);
--color-text-secondary: #c4cfdb;
--color-text-secondary: var(--text-secondary);
--color-text-muted: var(--ui-text-muted);
--color-text-on-primary: #10161d;
--color-text-on-primary: var(--text-inverse);
--color-border-default: var(--ui-border-strong);
--color-border-subtle: var(--ui-border-soft);
/* Status colors */
--status-open: var(--ui-accent-info);
--status-ready: var(--ui-accent-ready);
--status-in-progress: var(--ui-accent-warning);
--status-progress: var(--ui-accent-warning);
--status-blocked: var(--ui-accent-blocked);
--status-blocked-earthy: var(--ui-accent-blocked);
--status-closed: #7f8b98;
--status-closed-earthy: #7f8b98;
--status-deferred: #7f8b98;
--status-open: var(--accent-info);
--status-progress: var(--status-in-progress);
--status-blocked-earthy: var(--status-blocked);
--status-closed-earthy: var(--status-closed);
/* Liveness colors */
--liveness-active: var(--ui-accent-ready);
--liveness-stale: var(--ui-accent-warning);
--liveness-stuck: var(--ui-accent-action-red);
--liveness-dead: var(--ui-accent-action-red);
--liveness-idle: #7f8b98;
--liveness-idle: var(--text-tertiary);
/* Agent Role Colors */
--agent-role-ui: #6B9BD2;
@ -83,10 +78,10 @@
--priority-p4: #64748b;
/* Blocks/Unlocks Section Colors */
--color-blocks-bg: rgba(212, 165, 116, 0.1);
--color-unlocks-bg: rgba(229, 115, 115, 0.1);
--color-blocks-border: rgba(212, 165, 116, 0.2);
--color-unlocks-border: rgba(229, 115, 115, 0.2);
--color-blocks-bg: var(--status-in-progress);
--color-unlocks-bg: var(--status-blocked);
--color-blocks-border: color-mix(in srgb, var(--accent-warning) 35%, transparent);
--color-unlocks-border: color-mix(in srgb, var(--accent-danger) 35%, transparent);
/* ========== RADI ========== */
--radius-sm: 0.375rem;
@ -98,10 +93,8 @@
--radius-pill: 9999px;
/* ========== SHADOWS ========== */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
--shadow-soft-lg: 0 10px 30px -10px rgba(0, 0, 0, 0.3);
--shadow-soft-xl: 0 20px 40px -10px rgba(0, 0, 0, 0.4);
--shadow-soft-lg: var(--shadow-md);
--shadow-soft-xl: var(--shadow-lg);
/* ========== TYPOGRAPHY ========== */
--font-ui-stack: var(--ui-font-sans);
@ -139,13 +132,12 @@
--sidebar-right-width: 17.5rem;
--topbar-height: 3.75rem;
--glass-base: linear-gradient(180deg,
color-mix(in srgb, var(--ui-bg-card) 72%, black),
color-mix(in srgb, var(--ui-bg-panel) 78%, black));
--edge-top: color-mix(in srgb, var(--ui-border-strong) 80%, white 20%);
--edge-bottom: color-mix(in srgb, var(--ui-border-soft) 75%, black 25%);
--elevation-ambient: 0 20px 40px -16px rgba(0, 0, 0, 0.78);
--elevation-tight: 0 10px 24px -12px rgba(0, 0, 0, 0.7);
--glass-base: linear-gradient(180deg, var(--surface-elevated), var(--surface-secondary));
--edge-side: var(--ui-border-soft);
--edge-top: color-mix(in srgb, var(--ui-border-strong) 88%, white 12%);
--edge-bottom: color-mix(in srgb, var(--ui-border-soft) 88%, black 12%);
--elevation-ambient: var(--shadow-lg);
--elevation-tight: var(--shadow-md);
/* ========== LEGACY COMPATIBILITY TOKENS ========== */
/* For existing components that reference these */
@ -181,7 +173,7 @@ body {
* {
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.35) rgba(255, 255, 255, 0.02);
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
*::-webkit-scrollbar {
@ -190,18 +182,18 @@ body {
}
*::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.02);
background: var(--scrollbar-track);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(156, 163, 175, 0.55), rgba(107, 114, 128, 0.45));
border: 1px solid rgba(255, 255, 255, 0.1);
background: var(--scrollbar-thumb);
border: 1px solid color-mix(in srgb, var(--scrollbar-thumb) 70%, transparent);
border-radius: 9999px;
}
*::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(186, 194, 209, 0.72), rgba(124, 136, 156, 0.62));
background: var(--scrollbar-thumb-hover);
}
.custom-scrollbar::-webkit-scrollbar {
@ -210,12 +202,12 @@ body {
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.15);
background: var(--scrollbar-thumb);
border-radius: 20px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(148, 163, 184, 0.3);
background: var(--scrollbar-thumb-hover);
}
.no-scrollbar::-webkit-scrollbar {
@ -229,14 +221,14 @@ body {
.workflow-card {
border: 1px solid rgba(255, 255, 255, 0.08);
border-top-color: rgba(255, 255, 255, 0.24);
border-bottom-color: rgba(0, 0, 0, 0.9);
background: linear-gradient(180deg, rgba(42, 44, 52, 0.6) 0%, rgba(22, 23, 28, 0.6) 100%);
border: 1px solid var(--border-default);
border-top-color: color-mix(in srgb, var(--border-strong) 88%, white 12%);
border-bottom-color: color-mix(in srgb, var(--border-subtle) 88%, black 12%);
background: linear-gradient(180deg, var(--surface-quaternary) 0%, var(--surface-secondary) 100%);
box-shadow:
var(--elevation-ambient),
var(--elevation-tight),
inset 0 1px 1px rgba(255, 255, 255, 0.15);
inset 0 1px 1px color-mix(in srgb, var(--alpha-white-high) 40%, transparent);
backdrop-filter: blur(24px) saturate(120%);
-webkit-backdrop-filter: blur(24px) saturate(120%);
transform: translateZ(0);
@ -244,14 +236,14 @@ body {
}
.workflow-card-selected {
border-color: rgba(96, 165, 250, 0.42);
border-top-color: rgba(96, 165, 250, 0.58);
background: linear-gradient(180deg, rgba(60, 68, 88, 0.7) 0%, rgba(35, 40, 52, 0.7) 100%);
border-color: color-mix(in srgb, var(--accent-info) 42%, var(--border-default));
border-top-color: color-mix(in srgb, var(--accent-info) 58%, var(--border-strong));
background: linear-gradient(180deg, color-mix(in srgb, var(--surface-active) 55%, var(--surface-elevated)), var(--surface-secondary));
box-shadow:
0 20px 48px -8px rgba(0, 0, 0, 0.9),
0 0 0 1px rgba(96, 165, 250, 0.25),
0 0 40px rgba(96, 165, 250, 0.15),
inset 0 1px 1px rgba(255, 255, 255, 0.3);
var(--shadow-lg),
0 0 0 1px color-mix(in srgb, var(--accent-info) 25%, transparent),
var(--glow-info),
inset 0 1px 1px color-mix(in srgb, var(--alpha-white-high) 50%, transparent);
}
.glass-panel {
@ -265,19 +257,19 @@ body {
}
.bg-earthy-gradient {
background: linear-gradient(to bottom right, #2D2D2D, #363636);
background: linear-gradient(to bottom right, var(--surface-secondary), var(--surface-tertiary));
}
/* Shared dark form controls to avoid white-on-white browser defaults */
/* Shared themed form controls */
.ui-field {
border: 1px solid rgba(255, 255, 255, 0.1);
border: 1px solid var(--border-default);
border-top-color: var(--edge-top);
border-bottom-color: var(--edge-bottom);
background: linear-gradient(180deg, rgba(32, 34, 42, 0.72), rgba(17, 19, 26, 0.72));
background: linear-gradient(180deg, var(--surface-input), var(--surface-secondary));
color: var(--color-text-strong);
box-shadow:
0 8px 20px -12px rgba(0, 0, 0, 0.85),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
var(--shadow-sm),
inset 0 1px 0 color-mix(in srgb, var(--alpha-white-high) 22%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
@ -288,10 +280,10 @@ body {
.ui-field:focus-visible {
outline: none;
border-color: rgba(96, 165, 250, 0.48);
border-color: color-mix(in srgb, var(--accent-info) 48%, var(--border-default));
box-shadow:
0 0 0 2px rgba(96, 165, 250, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
0 0 0 2px color-mix(in srgb, var(--accent-info) 20%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--alpha-white-high) 22%, transparent);
}
.ui-select {
@ -326,19 +318,19 @@ body {
}
.ui-shell-topbar {
background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-panel) 92%, black), var(--ui-bg-shell));
background: linear-gradient(180deg, var(--ui-bg-panel), var(--ui-bg-shell));
border-bottom: 1px solid color-mix(in srgb, var(--ui-accent-info) 22%, var(--ui-border-soft));
box-shadow: 0 10px 24px -20px rgba(0, 0, 0, 0.9);
box-shadow: var(--shadow-sm);
}
.ui-shell-middle {
background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-app) 74%, black), color-mix(in srgb, var(--ui-bg-app) 90%, black));
background: linear-gradient(180deg, var(--ui-bg-app), var(--ui-bg-main));
border-left: 1px solid color-mix(in srgb, var(--ui-accent-info) 20%, var(--ui-border-soft));
border-right: 1px solid color-mix(in srgb, var(--ui-accent-info) 20%, var(--ui-border-soft));
}
.ui-shell-panel {
background: linear-gradient(180deg, color-mix(in srgb, var(--ui-bg-shell) 86%, black), color-mix(in srgb, var(--ui-bg-panel) 84%, black));
background: linear-gradient(180deg, var(--ui-bg-shell), var(--ui-bg-panel));
border-left: 1px solid color-mix(in srgb, var(--ui-accent-info) 30%, var(--ui-border-soft));
}
@ -349,8 +341,8 @@ body {
.workflow-graph-legend {
backdrop-filter: blur(12px);
background: rgba(20, 23, 31, 0.72);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 8px 32px rgba(0, 0, 0, 0.3);
background: color-mix(in srgb, var(--surface-overlay) 78%, transparent);
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--alpha-white-high) 22%, transparent), var(--shadow-md);
}
.workflow-graph-flow .react-flow__viewport {

View file

@ -1,39 +0,0 @@
import { DependencyGraphPage } from '../../components/graph/dependency-graph-page';
export const dynamic = 'force-dynamic';
import { readIssuesForScope } from '../../lib/aggregate-read';
import { resolveProjectScope } from '../../lib/project-scope';
import { listProjects } from '../../lib/registry';
interface GraphPageProps {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
}
export default async function GraphPage({ searchParams }: GraphPageProps) {
const params = (await searchParams) ?? {};
const requestedProjectKey = typeof params.project === 'string' ? params.project : null;
const requestedMode = typeof params.mode === 'string' ? params.mode : null;
const registryProjects = await listProjects();
const scope = resolveProjectScope({
currentProjectRoot: process.cwd(),
registryProjects,
requestedProjectKey,
requestedMode,
});
const issues = await readIssuesForScope({
mode: scope.mode,
selected: scope.selected,
scopeOptions: scope.options,
preferBd: true,
});
return (
<DependencyGraphPage
issues={issues}
projectRoot={scope.selected.root}
projectScopeKey={scope.selected.key}
projectScopeOptions={scope.options}
projectScopeMode={scope.mode}
/>
);
}

View file

@ -1,556 +0,0 @@
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useRouter, useSearchParams } from "next/navigation"
import { ArrowLeft, ArrowUpRight, Clock3, Link2, MessageCircle, TriangleAlert, X } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
type TaskStatus = "open" | "in_progress" | "blocked" | "deferred" | "closed"
type Task = {
id: string
title: string
description: string
status: TaskStatus
priority: 0 | 1 | 2 | 3 | 4
issueType: string
assignee: string
owner: string
labels: string[]
blockedReason: string
updatedAgo: string
dependencyCount: number
blockedByCount: number
commentCount: number
unread: boolean
}
type Epic = {
id: string
name: string
progress: number
openCount: number
tasks: Task[]
}
const palette = {
primary: "#F2A62F",
secondary: "#00D1A8",
accent: "#0FC5AE",
eggplant: "#4A2F63",
bg: "#2D2E3C",
surface: "#333341",
border: "#4A4D5C",
text: "#EDEBE5",
textSecondary: "#B8B7B1",
mutedBg: "#2F2F3E",
success: "#0FC5AE",
warning: "#D28A2C",
error: "#D64545",
info: "#00D1A8",
atmosphereWarm: "#5A4632",
atmosphereCool: "#23484D",
}
const initialEpics: Epic[] = [
{
id: "bb-ui2",
name: "Unified UX - Earthy Dark Shell",
progress: 69,
openCount: 11,
tasks: [
{ id: "bb-atf", title: "Agent swarm-view-integrator", description: "Integrate swarm view into social workroom shell.", status: "open", priority: 1, issueType: "task", assignee: "sarah.lee", owner: "swarm-team", labels: ["social", "swarm"], blockedReason: "", updatedAgo: "8m", dependencyCount: 1, blockedByCount: 0, commentCount: 3, unread: true },
{ id: "bb-z6s", title: "Agent social-view-integrator", description: "Wire social stream cards and panel routing.", status: "in_progress", priority: 0, issueType: "feature", assignee: "alex.chen", owner: "social-team", labels: ["social", "ui"], blockedReason: "", updatedAgo: "14m", dependencyCount: 2, blockedByCount: 0, commentCount: 7, unread: true },
{ id: "bb-nuy", title: "Agent swarm-card-builder", description: "Build consistent swarm card visuals and metadata.", status: "blocked", priority: 0, issueType: "bug", assignee: "alex.chen", owner: "swarm-team", labels: ["swarm", "cards"], blockedReason: "Waiting on dependency bb-ui2.0", updatedAgo: "35m", dependencyCount: 3, blockedByCount: 1, commentCount: 5, unread: true },
{ id: "bb-3ha", title: "Agent sessions-integrator", description: "Session metrics panel integrated and verified.", status: "closed", priority: 2, issueType: "chore", assignee: "alex.chen", owner: "sessions-team", labels: ["sessions"], blockedReason: "", updatedAgo: "2h", dependencyCount: 0, blockedByCount: 0, commentCount: 4, unread: false },
],
},
{
id: "bb-xhm",
name: "Timeline and Activity Feed",
progress: 80,
openCount: 5,
tasks: [
{ id: "bb-3dv", title: "Agent rightpanel-builder", description: "Implement right rail card stack and compact activity.", status: "open", priority: 2, issueType: "task", assignee: "alex.chen", owner: "layout-team", labels: ["layout", "right-panel"], blockedReason: "", updatedAgo: "11m", dependencyCount: 1, blockedByCount: 0, commentCount: 1, unread: true },
{ id: "bb-dwz", title: "Agent leftpanel-builder", description: "Epic->task navigation with search and metadata icons.", status: "in_progress", priority: 1, issueType: "feature", assignee: "sarah.lee", owner: "layout-team", labels: ["layout", "left-panel"], blockedReason: "", updatedAgo: "19m", dependencyCount: 0, blockedByCount: 0, commentCount: 6, unread: true },
{ id: "bb-5am", title: "Agent topbar-builder", description: "Topbar controls and filter sync.", status: "blocked", priority: 1, issueType: "bug", assignee: "agent-007", owner: "layout-team", labels: ["topbar"], blockedReason: "Navigation contract mismatch", updatedAgo: "41m", dependencyCount: 2, blockedByCount: 1, commentCount: 2, unread: false },
{ id: "bb-z2l", title: "Agent mobile-nav-builder", description: "Mobile drawer flow for three-pane shell.", status: "deferred", priority: 1, issueType: "task", assignee: "sarah.lee", owner: "mobile-team", labels: ["mobile", "navigation"], blockedReason: "", updatedAgo: "52m", dependencyCount: 0, blockedByCount: 0, commentCount: 2, unread: false },
],
},
]
function statusClasses(status: TaskStatus) {
if (status === "in_progress") return "border-l-[3px] border-l-[#0FC5AE] bg-[linear-gradient(145deg,#333341,#2F2F3E)]"
if (status === "blocked") return "border-l-[3px] border-l-[#D64545] bg-[linear-gradient(145deg,#333341,#302B31)]"
if (status === "deferred") return "border-l-[3px] border-l-[#D28A2C] bg-[linear-gradient(145deg,#333341,#342F29)]"
if (status === "closed") return "border-l-[3px] border-l-[#6D6F7B] bg-[linear-gradient(145deg,#333341,#2F3039)]"
return "border-l-[3px] border-l-[#00D1A8] bg-[linear-gradient(145deg,#333341,#2D313D)]"
}
function statusBadge(status: TaskStatus) {
if (status === "in_progress") return "bg-[#0FC5AE] text-[#0E2220]"
if (status === "blocked") return "bg-[#D64545] text-white"
if (status === "deferred") return "bg-[#D28A2C] text-[#24190C]"
if (status === "closed") return "bg-[#5A5D6A] text-[#D4D6DE]"
return "bg-[#00D1A8] text-[#07221C]"
}
const panelClass = "rounded-2xl border shadow-[0_16px_40px_rgba(0,0,0,0.28)] backdrop-blur-[2px]"
const subPanelClass = "rounded-xl border"
function updateQuery(searchParams: URLSearchParams, updates: Record<string, string | null>) {
const next = new URLSearchParams(searchParams.toString())
for (const [key, value] of Object.entries(updates)) {
if (!value) next.delete(key)
else next.set(key, value)
}
const qs = next.toString()
return qs ? `?${qs}` : "?"
}
export default function MockupPage() {
const searchParams = useSearchParams()
const router = useRouter()
const [epics, setEpics] = useState(initialEpics)
const [query, setQuery] = useState("")
const [leftMode, setLeftMode] = useState<"epics" | "tasks">("epics")
const urlEpic = searchParams.get("epic")
const urlTask = searchParams.get("task")
const urlThread = searchParams.get("thread") === "open"
const initialEpic = epics.find((epic) => epic.id === urlEpic) ?? epics[0]
const [selectedEpicId, setSelectedEpicId] = useState(initialEpic.id)
const [selectedTaskId, setSelectedTaskId] = useState(urlTask ?? initialEpic.tasks[0].id)
const [threadOpen, setThreadOpen] = useState(urlThread)
const [threadEditMode, setThreadEditMode] = useState(false)
const [draftTitle, setDraftTitle] = useState("")
const [draftDescription, setDraftDescription] = useState("")
const [draftStatus, setDraftStatus] = useState<TaskStatus>("open")
const [draftPriority, setDraftPriority] = useState<0 | 1 | 2 | 3 | 4>(2)
const [draftIssueType, setDraftIssueType] = useState("")
const [draftAssignee, setDraftAssignee] = useState("")
const [draftOwner, setDraftOwner] = useState("")
const [draftLabels, setDraftLabels] = useState("")
const [draftBlockedReason, setDraftBlockedReason] = useState("")
const [savePulse, setSavePulse] = useState(false)
const closeThread = useCallback(() => {
setThreadOpen(false)
setThreadEditMode(false)
}, [])
useEffect(() => {
const next = updateQuery(searchParams, {
epic: selectedEpicId,
task: selectedTaskId,
thread: threadOpen ? "open" : null,
})
router.replace(next, { scroll: false })
}, [router, searchParams, selectedEpicId, selectedTaskId, threadOpen])
useEffect(() => {
if (!threadOpen) {
return
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeThread()
}
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [threadOpen, closeThread])
const selectedEpic = epics.find((epic) => epic.id === selectedEpicId) ?? epics[0]
const filteredTasks = useMemo(() => {
const q = query.trim().toLowerCase()
return selectedEpic.tasks.filter((task) => `${task.id} ${task.title}`.toLowerCase().includes(q))
}, [query, selectedEpic.tasks])
const selectedTask = filteredTasks.find((task) => task.id === selectedTaskId) ?? filteredTasks[0]
useEffect(() => {
if (!selectedTask) return
setDraftTitle(selectedTask.title)
setDraftDescription(selectedTask.description)
setDraftStatus(selectedTask.status)
setDraftPriority(selectedTask.priority)
setDraftIssueType(selectedTask.issueType)
setDraftAssignee(selectedTask.assignee)
setDraftOwner(selectedTask.owner)
setDraftLabels(selectedTask.labels.join(", "))
setDraftBlockedReason(selectedTask.blockedReason)
setThreadEditMode(false)
}, [selectedTask, selectedTask?.id])
const saveTaskChanges = () => {
if (!selectedTask) return
const nextLabels = draftLabels
.split(",")
.map((part) => part.trim())
.filter(Boolean)
setEpics((current) =>
current.map((epic) =>
epic.id !== selectedEpicId
? epic
: {
...epic,
tasks: epic.tasks.map((task) =>
task.id !== selectedTask.id
? task
: {
...task,
title: draftTitle,
description: draftDescription,
status: draftStatus,
priority: draftPriority,
issueType: draftIssueType,
assignee: draftAssignee,
owner: draftOwner,
labels: nextLabels,
blockedReason: draftBlockedReason,
updatedAgo: "now",
blockedByCount: draftStatus === "blocked" ? Math.max(task.blockedByCount, 1) : 0,
}
),
}
)
)
setSavePulse(true)
setTimeout(() => setSavePulse(false), 900)
}
return (
<main className="min-h-screen" style={{ backgroundColor: palette.bg, color: palette.text }}>
<div
className="min-h-screen"
style={{
backgroundImage:
`radial-gradient(circle at 12% 16%, rgba(90,70,50,0.55), transparent 34%), radial-gradient(circle at 88% 82%, rgba(35,72,77,0.50), transparent 32%)`,
}}
>
<div className="mx-auto max-w-[1500px] px-4 py-6 md:px-8 md:py-8">
<header className="mb-6 flex flex-wrap items-end justify-between gap-4">
<div>
<h1 className="text-4xl font-semibold tracking-tight leading-[1.02] md:text-5xl">Social Workroom</h1>
<p className="mt-2 text-sm" style={{ color: palette.textSecondary }}>Task-first center. Epic drill-in. Live awareness rail.</p>
</div>
<Badge className="rounded-full px-3 py-1 text-xs text-white" style={{ backgroundColor: palette.eggplant }}>mockup route</Badge>
</header>
<section className="grid gap-4 lg:grid-cols-[24%_52%_24%]">
<Card className={panelClass} style={{ backgroundColor: palette.surface, borderColor: palette.border }}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
{leftMode === "tasks" ? (
<Button variant="ghost" className="h-8 px-2" onClick={() => setLeftMode("epics")}>
<ArrowLeft className="mr-2 h-4 w-4" /> Back to epics
</Button>
) : (
<CardTitle className="text-lg">Epics</CardTitle>
)}
<Badge className="rounded-full" style={{ backgroundColor: palette.mutedBg, color: palette.textSecondary }}>{selectedEpic.openCount} open</Badge>
</div>
<CardDescription style={{ color: palette.textSecondary }}>Select an epic, then choose a task.</CardDescription>
</CardHeader>
<CardContent className="pt-0">
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={leftMode === "epics" ? "Search epics" : "Search tasks"}
className="mb-3"
style={{ backgroundColor: palette.mutedBg, borderColor: palette.border }}
/>
<ScrollArea className="h-[520px] pr-2">
<div className="space-y-2">
{leftMode === "epics"
? epics
.filter((epic) => epic.name.toLowerCase().includes(query.toLowerCase()))
.map((epic) => (
<button
key={epic.id}
type="button"
className={`${subPanelClass} w-full p-3 text-left transition duration-200 hover:-translate-y-[1px] hover:shadow-[0_10px_24px_rgba(0,0,0,0.35)]`}
style={{ backgroundColor: palette.surface, borderColor: palette.border }}
onClick={() => {
setSelectedEpicId(epic.id)
setSelectedTaskId(epic.tasks[0]?.id ?? "")
setLeftMode("tasks")
closeThread()
}}
>
<p className="text-sm font-semibold">{epic.name}</p>
<p className="mt-1 text-xs" style={{ color: palette.textSecondary }}>{epic.id}</p>
</button>
))
: filteredTasks.map((task) => (
<button
key={task.id}
type="button"
onClick={() => {
setSelectedTaskId(task.id)
closeThread()
}}
className={`${subPanelClass} w-full p-3 text-left transition duration-200 ${selectedTask?.id === task.id
? "shadow-[0_12px_26px_rgba(0,0,0,0.4)]"
: "hover:-translate-y-[1px] hover:shadow-[0_10px_22px_rgba(0,0,0,0.33)]"
}`}
style={{
backgroundColor: selectedTask?.id === task.id ? palette.mutedBg : palette.surface,
borderColor: selectedTask?.id === task.id ? palette.primary : palette.border,
}}
>
<p className="text-sm font-semibold">{task.id}</p>
<p className="mt-1 line-clamp-1 text-xs" style={{ color: palette.textSecondary }}>{task.title}</p>
<div className="mt-2 flex items-center gap-3 text-[11px]" style={{ color: palette.textSecondary }}>
<span className="inline-flex items-center gap-1"><Clock3 className="h-3 w-3" />{task.updatedAgo}</span>
<span className="inline-flex items-center gap-1"><Link2 className="h-3 w-3" />{task.dependencyCount}</span>
<span className="inline-flex items-center gap-1"><MessageCircle className="h-3 w-3" />{task.commentCount}</span>
</div>
</button>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
<Card className={panelClass} style={{ backgroundColor: palette.surface, borderColor: palette.border }}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg">{selectedEpic.name}</CardTitle>
<CardDescription style={{ color: palette.textSecondary }}>Task cards + thread context</CardDescription>
</div>
<Button className="h-8 rounded-full px-4 text-white" style={{ backgroundColor: palette.primary }}>New update</Button>
</div>
</CardHeader>
<CardContent className="pt-0">
<ScrollArea className="h-[430px] pr-2">
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
{filteredTasks.map((task) => (
<button
key={task.id}
type="button"
onClick={() => setSelectedTaskId(task.id)}
className={`rounded-xl border p-4 text-left transition duration-200 hover:-translate-y-[1px] hover:shadow-[0_14px_28px_rgba(0,0,0,0.35)] ${statusClasses(task.status)}`}
style={{ borderColor: selectedTask?.id === task.id ? palette.primary : palette.border }}
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold" style={{ color: palette.eggplant }}>{task.id}</span>
<Badge className={`rounded-full px-2 py-0.5 text-[11px] ${statusBadge(task.status)}`}>{task.status.replace("_", " ")}</Badge>
</div>
<p className="mt-3 text-[1.7rem] font-semibold leading-[1.15]">{task.title}</p>
<p className="mt-2 line-clamp-2 text-sm" style={{ color: palette.textSecondary }}>{task.description}</p>
<div className="mt-4 flex items-center gap-3 text-xs" style={{ color: palette.textSecondary }}>
<span className="inline-flex items-center gap-1"><Clock3 className="h-3.5 w-3.5" />{task.updatedAgo}</span>
<span className="inline-flex items-center gap-1"><Link2 className="h-3.5 w-3.5" />{task.dependencyCount}</span>
<span className="inline-flex items-center gap-1"><MessageCircle className="h-3.5 w-3.5" />{task.commentCount}</span>
</div>
</button>
))}
</div>
</ScrollArea>
<Separator className="my-4" />
<div className={`${subPanelClass} p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]`} style={{ borderColor: palette.border, backgroundColor: palette.mutedBg }}>
<div className="mb-2 flex items-center justify-between">
<p className="text-sm font-semibold">Conversation: {selectedTask?.id}</p>
<Button variant="ghost" className="h-7 px-2" style={{ color: palette.secondary }} onClick={() => setThreadOpen(true)}>
Open thread <ArrowUpRight className="ml-1 h-3.5 w-3.5" />
</Button>
</div>
<div className="space-y-2">
<div className="rounded-lg border px-3 py-2 text-sm" style={{ borderColor: "#5A5D6A", backgroundColor: "#2A2B37" }}>
<span className="font-semibold" style={{ color: palette.success }}>alex.chen</span>
<span className="mx-1 text-xs" style={{ color: "#8F92A3" }}>2m</span>
<span style={{ color: palette.textSecondary }}>Need confirmation that detail strip stays sticky while card grid scrolls.</span>
</div>
<div className="rounded-lg border px-3 py-2 text-sm" style={{ borderColor: "#5A5D6A", backgroundColor: "#2A2B37" }}>
<span className="font-semibold" style={{ color: palette.secondary }}>sarah.lee</span>
<span className="mx-1 text-xs" style={{ color: "#8F92A3" }}>1m</span>
<span style={{ color: palette.textSecondary }}>Approved if right rail remains visible at 1280px breakpoint.</span>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className={panelClass} style={{ backgroundColor: palette.surface, borderColor: palette.border }}>
<CardHeader className="pb-3">
<CardTitle className="text-lg">Live Context</CardTitle>
<CardDescription style={{ color: palette.textSecondary }}>Persistent awareness while working tasks.</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-0">
<div className={`${subPanelClass} p-3 shadow-[0_8px_20px_rgba(0,0,0,0.25)]`} style={{ borderColor: palette.border, backgroundColor: palette.mutedBg }}>
<p className="mb-2 text-sm font-semibold">Live Agents</p>
<div className="space-y-1 text-sm">
<p className="flex items-center justify-between"><span>swarm-view-integrator</span><span style={{ color: palette.success }}>online</span></p>
<p className="flex items-center justify-between"><span>social-view-integrator</span><span style={{ color: palette.warning }}>away</span></p>
<p className="flex items-center justify-between"><span>graph-integrator</span><span style={{ color: palette.info }}>busy</span></p>
</div>
</div>
<div className={`${subPanelClass} p-3 shadow-[0_8px_20px_rgba(0,0,0,0.25)]`} style={{ borderColor: palette.border, backgroundColor: palette.mutedBg }}>
<p className="mb-2 text-sm font-semibold">Recent Activity</p>
<div className="space-y-1 text-xs" style={{ color: palette.textSecondary }}>
<p>5m · bb-z6s moved to in progress</p>
<p>11m · bb-atf received 2 comments</p>
<p>18m · bb-3ha marked closed</p>
<p>33m · bb-nuy dependency changed</p>
</div>
</div>
<div className={`${subPanelClass} p-3 shadow-[0_8px_20px_rgba(0,0,0,0.22)]`} style={{ borderColor: "#6A4E2F", backgroundColor: "#3A332B" }}>
<p className="mb-2 text-sm font-semibold">Attention</p>
<p className="flex items-center gap-2 text-sm" style={{ color: "#F2C684" }}><TriangleAlert className="h-4 w-4" /> 2 blocked tasks in selected epic</p>
</div>
</CardContent>
</Card>
</section>
</div>
</div>
{threadOpen ? (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/35 p-2 md:items-center md:p-4" onClick={closeThread}>
<div
className={`${panelClass} w-full max-w-[980px] p-4 md:p-5`}
style={{
borderColor: "#5B5E71",
background: "linear-gradient(180deg,#323342,#2A2B38)",
color: palette.text,
}}
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center justify-between">
<div>
<p className="text-lg font-semibold">Thread · {selectedTask?.id}</p>
<p className="text-xs" style={{ color: palette.textSecondary }}>Bead summary and inline edit mode</p>
</div>
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-white/10" aria-label="Close thread" onClick={closeThread}><X className="h-4 w-4" /></Button>
</div>
<div className="mt-3 rounded-xl border p-4" style={{ borderColor: "#55586A", backgroundColor: "#2A2B37" }}>
<div className="mb-3 flex items-center justify-between">
<p className="text-sm font-semibold">{threadEditMode ? "Edit task" : "Task summary"}</p>
<Badge className="rounded-full px-2 py-0.5 text-[11px]" style={{ backgroundColor: savePulse ? palette.success : "#3C3E4E", color: savePulse ? "#0E2220" : "#B8B7B1" }}>
{savePulse ? "saved" : "ready"}
</Badge>
</div>
{!threadEditMode ? (
<div className="space-y-3 text-sm">
<div className="rounded-lg border p-3" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>
<p className="text-xs mb-1" style={{ color: "#A4A7B7" }}>{selectedTask?.id}</p>
<p className="font-semibold text-base text-[#ECEBE5]">{selectedTask?.title}</p>
<p className="mt-1" style={{ color: palette.textSecondary }}>{selectedTask?.description}</p>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Status: {selectedTask?.status}</div>
<div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Priority: P{selectedTask?.priority}</div>
<div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Assignee: {selectedTask?.assignee || "-"}</div>
<div className="rounded-lg border p-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>Owner: {selectedTask?.owner || "-"}</div>
<div className="rounded-lg border p-2 col-span-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>
Labels: {selectedTask?.labels.join(", ") || "-"}
</div>
<div className="rounded-lg border p-2 col-span-2" style={{ borderColor: "#585B6D", backgroundColor: "#323342" }}>
Blocked reason: {selectedTask?.blockedReason || "None"}
</div>
</div>
<div className="flex justify-end">
<Button className="rounded-full px-4 text-[#24190C]" style={{ backgroundColor: palette.primary }} onClick={() => setThreadEditMode(true)}>
Edit
</Button>
</div>
</div>
) : (
<>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div>
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Title</p>
<Input value={draftTitle} onChange={(event) => setDraftTitle(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
</div>
<div>
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Assignee</p>
<Input value={draftAssignee} onChange={(event) => setDraftAssignee(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
</div>
<div className="md:col-span-2">
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Description</p>
<Input value={draftDescription} onChange={(event) => setDraftDescription(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
</div>
<div>
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Issue type</p>
<Input value={draftIssueType} onChange={(event) => setDraftIssueType(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
</div>
<div>
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Owner</p>
<Input value={draftOwner} disabled style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text, opacity: 0.7 }} />
</div>
<div className="md:col-span-2">
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Labels (comma separated)</p>
<Input value={draftLabels} onChange={(event) => setDraftLabels(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
</div>
</div>
<div className="mt-3">
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Status</p>
<div className="flex flex-wrap gap-2">
{(["open", "in_progress", "blocked", "deferred", "closed"] as TaskStatus[]).map((status) => (
<button
key={status}
type="button"
className={`rounded-full border px-2 py-1 text-xs ${draftStatus === status ? statusBadge(status) : ""}`}
style={{
borderColor: draftStatus === status ? "transparent" : palette.border,
backgroundColor: draftStatus === status ? undefined : "#323342",
color: draftStatus === status ? undefined : palette.textSecondary,
}}
onClick={() => setDraftStatus(status)}
>
{status.replace("_", " ")}
</button>
))}
</div>
</div>
<div className="mt-3">
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Priority</p>
<div className="flex flex-wrap gap-2">
{([0, 1, 2, 3, 4] as const).map((priority) => (
<button
key={priority}
type="button"
onClick={() => setDraftPriority(priority)}
className="rounded-full border px-2 py-1 text-xs"
style={{
borderColor: draftPriority === priority ? palette.eggplant : palette.border,
backgroundColor: draftPriority === priority ? "#F4EAFE" : palette.surface,
color: draftPriority === priority ? palette.eggplant : palette.textSecondary,
}}
>
P{priority}
</button>
))}
</div>
</div>
<div className="mt-3">
<p className="mb-1 text-xs" style={{ color: palette.textSecondary }}>Blocked reason</p>
<Input value={draftBlockedReason} onChange={(event) => setDraftBlockedReason(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} />
</div>
<div className="mt-4 flex justify-end gap-2">
<Button variant="outline" className="rounded-full px-4 border-[#585B6D] bg-[#323342] text-[#B8B7B1] hover:bg-[#3A3B49]" onClick={() => setThreadEditMode(false)}>
Cancel
</Button>
<Button className="rounded-full px-4 text-[#24190C]" style={{ backgroundColor: palette.primary }} onClick={() => { saveTaskChanges(); setThreadEditMode(false) }}>
Save changes
</Button>
</div>
</>
)}
</div>
</div>
</div>
) : null}
</main>
)
}

View file

@ -1,37 +0,0 @@
import { KanbanPage } from '../components/kanban/kanban-page';
import { readIssuesForScope } from '../lib/aggregate-read';
import { resolveProjectScope } from '../lib/project-scope';
import { listProjects } from '../lib/registry';
interface PageProps {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
}
export default async function Page({ searchParams }: PageProps) {
const params = (await searchParams) ?? {};
const requestedProjectKey = typeof params.project === 'string' ? params.project : null;
const requestedMode = typeof params.mode === 'string' ? params.mode : null;
const registryProjects = await listProjects();
const scope = resolveProjectScope({
currentProjectRoot: process.cwd(),
registryProjects,
requestedProjectKey,
requestedMode,
});
const issues = await readIssuesForScope({
mode: scope.mode,
selected: scope.selected,
scopeOptions: scope.options,
preferBd: true,
});
return (
<KanbanPage
issues={issues}
projectRoot={scope.selected.root}
projectScopeKey={scope.selected.key}
projectScopeOptions={scope.options}
projectScopeMode={scope.mode}
/>
);
}

View file

@ -1,5 +0,0 @@
import { redirect } from 'next/navigation';
export default function SessionsRedirectPage() {
redirect('/?view=social');
}

View file

@ -1,60 +0,0 @@
'use client';
import { useEffect } from 'react';
import { TimelineFeed } from '../../components/timeline/timeline-feed';
import { useTimelineStore } from '../../components/timeline/timeline-store';
export default function TimelinePage() {
return (
<div className="mx-auto max-w-3xl px-4 py-8">
<header className="mb-8">
<h1 className="text-2xl font-bold text-text-strong">Activity Timeline</h1>
<p className="text-text-muted">Real-time stream of project mutations.</p>
</header>
<TimelineControls />
<TimelineSubscription />
<TimelineFeed />
</div>
);
}
function TimelineControls() {
return (
<div className="mb-6 flex gap-2">
{/* Placeholder for future filters */}
<div className="text-sm text-text-muted">Showing all activity</div>
</div>
);
}
function TimelineSubscription() {
const { addEvent, setHistory } = useTimelineStore();
useEffect(() => {
// 1. Fetch history
fetch('/api/activity')
.then(res => {
if (!res.ok) throw new Error('History fetch failed');
return res.json();
})
.then(data => setHistory(data))
.catch(err => console.error('Failed to load history', err));
// 2. Subscribe to SSE
const es = new EventSource('/api/events');
es.addEventListener('activity', (e) => {
try {
const event = JSON.parse(e.data);
addEvent(event);
} catch (err) {
console.error('Failed to parse activity event', err);
}
});
return () => es.close();
}, [setHistory, addEvent]);
return null;
}