chore: checkpoint before DAG views UX overhaul
This commit is contained in:
parent
5695125a75
commit
a03def1ca1
125 changed files with 40711 additions and 581 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import { activityEventBus } from '../../../lib/realtime';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const projectRoot = url.searchParams.get('projectRoot') || undefined;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { readIssuesFromDisk } from '../../../../../lib/read-issues';
|
|||
import { activityEventBus } from '../../../../../lib/realtime';
|
||||
import { getAgentMetrics } from '../../../../../lib/agent-sessions';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ agentId: string }> }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { listAgents } from '../../../../lib/agent-registry';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { readInteractionsViaBd } from '../../../../../lib/read-interactions';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { NextResponse } from 'next/server';
|
|||
|
||||
import { readIssuesFromDisk } from '../../../../lib/read-issues';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const projectRoot = url.searchParams.get('projectRoot') ?? process.cwd();
|
||||
|
|
|
|||
|
|
@ -21,11 +21,15 @@ async function readLastTouchedVersion(filePath: string): Promise<number | null>
|
|||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const projectRootSearchParam = url.searchParams.get('projectRoot');
|
||||
const projectRoot = canonicalizeWindowsPath(projectRootSearchParam || process.cwd());
|
||||
|
||||
console.log(`[SSE /api/events] Connection request - raw param: "${projectRootSearchParam}", canonicalized: "${projectRoot}"`);
|
||||
|
||||
try {
|
||||
getIssuesWatchManager().startWatch(projectRoot);
|
||||
} catch (error) {
|
||||
|
|
@ -41,7 +45,7 @@ export async function GET(request: Request): Promise<Response> {
|
|||
);
|
||||
}
|
||||
|
||||
let cleanup = () => {};
|
||||
let cleanup = () => { };
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
|
|
@ -55,15 +59,20 @@ export async function GET(request: Request): Promise<Response> {
|
|||
|
||||
write(SSE_CONNECTED_FRAME);
|
||||
|
||||
console.log(`[SSE /api/events] Subscribing to event bus with projectRoot: ${projectRoot}`);
|
||||
const unsubscribeIssues = issuesEventBus.subscribe(
|
||||
(event) => {
|
||||
console.log('[SSE /api/events] Received ISSUES event from bus:', event.kind, 'projectRoot:', event.projectRoot);
|
||||
write(toSseFrame(event));
|
||||
},
|
||||
{ projectRoot },
|
||||
);
|
||||
|
||||
console.log(`[SSE /api/events] Subscriber count after subscribe: ${issuesEventBus.getSubscriberCount()}`);
|
||||
|
||||
const unsubscribeActivity = activityEventBus.subscribe(
|
||||
(event) => {
|
||||
console.log('[SSE /api/events] Received ACTIVITY event from bus');
|
||||
write(toActivitySseFrame(event));
|
||||
},
|
||||
{ projectRoot },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
|
@ -23,24 +25,24 @@ export async function GET(request: Request) {
|
|||
});
|
||||
|
||||
if (!headResult.success) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to fetch mission head' }, { status: 500 });
|
||||
return NextResponse.json({ ok: false, error: 'Failed to fetch mission head' }, { status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
const head = JSON.parse(headResult.stdout);
|
||||
let children = [];
|
||||
if (childrenResult.success && childrenResult.stdout.trim()) {
|
||||
children = JSON.parse(childrenResult.stdout);
|
||||
children = JSON.parse(childrenResult.stdout);
|
||||
}
|
||||
|
||||
const headObj = Array.isArray(head) ? head[0] : head;
|
||||
|
||||
|
||||
// Transform for graph view (if needed, or just return raw issues and let UI handle it)
|
||||
// The WorkflowGraph component expects BeadIssue[]
|
||||
const nodes = [headObj, ...children];
|
||||
|
||||
return NextResponse.json({ ok: true, data: { nodes } });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse graph data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
src/app/api/swarm/archetypes/[id]/route.ts
Normal file
53
src/app/api/swarm/archetypes/[id]/route.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { saveArchetype, deleteArchetype } from '../../../../../lib/server/beads-fs';
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!body.name || !body.systemPrompt) {
|
||||
return NextResponse.json(
|
||||
{ error: 'name and systemPrompt are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const archetype = await saveArchetype({
|
||||
id,
|
||||
name: body.name,
|
||||
description: body.description || '',
|
||||
systemPrompt: body.systemPrompt,
|
||||
capabilities: body.capabilities || [],
|
||||
color: body.color || '#3b82f6',
|
||||
createdAt: body.createdAt,
|
||||
isBuiltIn: body.isBuiltIn
|
||||
});
|
||||
|
||||
return NextResponse.json(archetype);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update archetype' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
await deleteArchetype(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete archetype';
|
||||
const status = message.includes('built-in') ? 403 : 404;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,38 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getArchetypes } from '../../../../lib/server/beads-fs';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getArchetypes, saveArchetype } from '../../../../lib/server/beads-fs';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
const data = await getArchetypes();
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!body.name || !body.systemPrompt) {
|
||||
return NextResponse.json(
|
||||
{ error: 'name and systemPrompt are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const archetype = await saveArchetype({
|
||||
name: body.name,
|
||||
description: body.description || '',
|
||||
systemPrompt: body.systemPrompt,
|
||||
capabilities: body.capabilities || [],
|
||||
color: body.color || '#3b82f6'
|
||||
});
|
||||
|
||||
return NextResponse.json(archetype, { status: 201 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to create archetype' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
|
@ -23,7 +25,7 @@ export async function GET(request: Request) {
|
|||
// If output is empty or not JSON array, handle gracefully
|
||||
const json = JSON.parse(result.stdout || '[]');
|
||||
return NextResponse.json({ ok: true, data: json });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse formulas' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { runBdCommand } from '../../../../lib/bridge';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const projectRoot = searchParams.get('projectRoot');
|
||||
|
|
@ -23,7 +25,7 @@ export async function GET(request: Request) {
|
|||
});
|
||||
|
||||
if (!epicResult.success) {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to fetch epic' }, { status: 500 });
|
||||
return NextResponse.json({ ok: false, error: 'Failed to fetch epic' }, { status: 500 });
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -31,15 +33,15 @@ export async function GET(request: Request) {
|
|||
// Handle list returning empty or error gracefully
|
||||
let children = [];
|
||||
if (childrenResult.success && childrenResult.stdout.trim()) {
|
||||
children = JSON.parse(childrenResult.stdout);
|
||||
// bd list returns array, bd show returns object (or array of 1)
|
||||
children = JSON.parse(childrenResult.stdout);
|
||||
// bd list returns array, bd show returns object (or array of 1)
|
||||
}
|
||||
|
||||
const epicObj = Array.isArray(epic) ? epic[0] : epic;
|
||||
const issues = [epicObj, ...children];
|
||||
|
||||
return NextResponse.json({ ok: true, data: issues });
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return NextResponse.json({ ok: false, error: 'Failed to parse graph data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ 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');
|
||||
|
|
@ -29,11 +31,11 @@ export async function GET(request: Request): Promise<Response> {
|
|||
const rawData = JSON.parse(result.stdout);
|
||||
// Filter out items that look like agents (start with "Agent:" or have gt:agent style IDs if discernible)
|
||||
// Real swarms/molecules usually don't start with "Agent:".
|
||||
const swarms = (rawData.swarms || []).filter((s: any) =>
|
||||
!s.title.startsWith('Agent: ') &&
|
||||
const swarms = (rawData.swarms || []).filter((s: any) =>
|
||||
!s.title.startsWith('Agent: ') &&
|
||||
!s.title.startsWith('Agent:')
|
||||
);
|
||||
|
||||
|
||||
return NextResponse.json({ ok: true, data: { swarms } });
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ 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');
|
||||
|
|
|
|||
59
src/app/api/swarm/templates/[id]/route.ts
Normal file
59
src/app/api/swarm/templates/[id]/route.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { saveTemplate, deleteTemplate } from '../../../../../lib/server/beads-fs';
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!body.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.team || !Array.isArray(body.team) || body.team.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'team must be a non-empty array' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const template = await saveTemplate({
|
||||
id,
|
||||
name: body.name,
|
||||
description: body.description || '',
|
||||
team: body.team,
|
||||
protoFormula: body.protoFormula,
|
||||
createdAt: body.createdAt,
|
||||
isBuiltIn: body.isBuiltIn
|
||||
});
|
||||
|
||||
return NextResponse.json(template);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update template' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
await deleteTemplate(id);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to delete template';
|
||||
const status = message.includes('built-in') ? 403 : 404;
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,44 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { getTemplates } from '../../../../lib/server/beads-fs';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getTemplates, saveTemplate } from '../../../../lib/server/beads-fs';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
const data = await getTemplates();
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validation
|
||||
if (!body.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'name is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.team || !Array.isArray(body.team) || body.team.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'team must be a non-empty array' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const template = await saveTemplate({
|
||||
name: body.name,
|
||||
description: body.description || '',
|
||||
team: body.team,
|
||||
protoFormula: body.protoFormula
|
||||
});
|
||||
|
||||
return NextResponse.json(template, { status: 201 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to create template' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export default function MockupPage() {
|
|||
setDraftLabels(selectedTask.labels.join(", "))
|
||||
setDraftBlockedReason(selectedTask.blockedReason)
|
||||
setThreadEditMode(false)
|
||||
}, [selectedTask?.id])
|
||||
}, [selectedTask, selectedTask?.id])
|
||||
|
||||
const saveTaskChanges = () => {
|
||||
if (!selectedTask) return
|
||||
|
|
@ -202,26 +202,26 @@ export default function MockupPage() {
|
|||
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,
|
||||
}
|
||||
),
|
||||
}
|
||||
...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)
|
||||
|
|
@ -250,29 +250,29 @@ export default function MockupPage() {
|
|||
<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>
|
||||
{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
|
||||
<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
|
||||
|
|
@ -291,7 +291,7 @@ export default function MockupPage() {
|
|||
<p className="mt-1 text-xs" style={{ color: palette.textSecondary }}>{epic.id}</p>
|
||||
</button>
|
||||
))
|
||||
: filteredTasks.map((task) => (
|
||||
: filteredTasks.map((task) => (
|
||||
<button
|
||||
key={task.id}
|
||||
type="button"
|
||||
|
|
@ -299,11 +299,10 @@ export default function MockupPage() {
|
|||
setSelectedTaskId(task.id)
|
||||
closeThread()
|
||||
}}
|
||||
className={`${subPanelClass} w-full p-3 text-left transition duration-200 ${
|
||||
selectedTask?.id === task.id
|
||||
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,
|
||||
|
|
@ -318,68 +317,68 @@ export default function MockupPage() {
|
|||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -389,27 +388,27 @@ export default function MockupPage() {
|
|||
<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 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>
|
||||
<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 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>
|
||||
</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue