diff --git a/.beads/.gitignore b/.beads/.gitignore index d27a1db..0acd8c6 100644 --- a/.beads/.gitignore +++ b/.beads/.gitignore @@ -35,7 +35,9 @@ beads.right.meta.json # Sync state (local-only, per-machine) # These files are machine-specific and should not be shared across clones .sync.lock +.jsonl.lock sync_base.jsonl +export-state/ # NOTE: Do NOT add negation patterns (e.g., !issues.jsonl) here. # They would override fork protection in .git/info/exclude, allowing diff --git a/.beads/bd.sock.startlock b/.beads/bd.sock.startlock new file mode 100644 index 0000000..90bfc8d --- /dev/null +++ b/.beads/bd.sock.startlock @@ -0,0 +1 @@ +58264 diff --git a/src/app/mockup/page.tsx b/src/app/mockup/page.tsx new file mode 100644 index 0000000..419c8c2 --- /dev/null +++ b/src/app/mockup/page.tsx @@ -0,0 +1,557 @@ +"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) { + 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("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?.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 ( +
+
+
+
+
+

Social Workroom

+

Task-first center. Epic drill-in. Live awareness rail.

+
+ mockup route +
+ +
+ + +
+ {leftMode === "tasks" ? ( + + ) : ( + Epics + )} + {selectedEpic.openCount} open +
+ Select an epic, then choose a task. +
+ + setQuery(event.target.value)} + placeholder={leftMode === "epics" ? "Search epics" : "Search tasks"} + className="mb-3" + style={{ backgroundColor: palette.mutedBg, borderColor: palette.border }} + /> + +
+ {leftMode === "epics" + ? epics + .filter((epic) => epic.name.toLowerCase().includes(query.toLowerCase())) + .map((epic) => ( + + )) + : filteredTasks.map((task) => ( + + ))} +
+
+
+
+ + + +
+
+ {selectedEpic.name} + Task cards + thread context +
+ +
+
+ + +
+ {filteredTasks.map((task) => ( + + ))} +
+
+ +
+
+

Conversation: {selectedTask?.id}

+ +
+
+
+ alex.chen + 2m + Need confirmation that detail strip stays sticky while card grid scrolls. +
+
+ sarah.lee + 1m + Approved if right rail remains visible at 1280px breakpoint. +
+
+
+
+
+ + + + Live Context + Persistent awareness while working tasks. + + +
+

Live Agents

+
+

swarm-view-integratoronline

+

social-view-integratoraway

+

graph-integratorbusy

+
+
+
+

Recent Activity

+
+

5m · bb-z6s moved to in progress

+

11m · bb-atf received 2 comments

+

18m · bb-3ha marked closed

+

33m · bb-nuy dependency changed

+
+
+
+

Attention

+

2 blocked tasks in selected epic

+
+
+
+
+
+
+ + {threadOpen ? ( +
+
event.stopPropagation()} + > +
+
+

Thread · {selectedTask?.id}

+

Bead summary and inline edit mode

+
+ +
+
+
+

{threadEditMode ? "Edit task" : "Task summary"}

+ + {savePulse ? "saved" : "ready"} + +
+ {!threadEditMode ? ( +
+
+

{selectedTask?.id}

+

{selectedTask?.title}

+

{selectedTask?.description}

+
+
+
Status: {selectedTask?.status}
+
Priority: P{selectedTask?.priority}
+
Assignee: {selectedTask?.assignee || "-"}
+
Owner: {selectedTask?.owner || "-"}
+
+ Labels: {selectedTask?.labels.join(", ") || "-"} +
+
+ Blocked reason: {selectedTask?.blockedReason || "None"} +
+
+
+ +
+
+ ) : ( + <> +
+
+

Title

+ setDraftTitle(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} /> +
+
+

Assignee

+ setDraftAssignee(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} /> +
+
+

Description

+ setDraftDescription(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} /> +
+
+

Issue type

+ setDraftIssueType(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} /> +
+
+

Owner

+ +
+
+

Labels (comma separated)

+ setDraftLabels(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} /> +
+
+
+

Status

+
+ {(["open", "in_progress", "blocked", "deferred", "closed"] as TaskStatus[]).map((status) => ( + + ))} +
+
+
+

Priority

+
+ {([0, 1, 2, 3, 4] as const).map((priority) => ( + + ))} +
+
+
+

Blocked reason

+ setDraftBlockedReason(event.target.value)} style={{ backgroundColor: "#323342", borderColor: "#585B6D", color: palette.text }} /> +
+
+ + +
+ + )} +
+
+
+ ) : null} +
+ ) +} diff --git a/src/components/shared/module-card.tsx b/src/components/shared/module-card.tsx index 77ea5c6..d0d9752 100644 --- a/src/components/shared/module-card.tsx +++ b/src/components/shared/module-card.tsx @@ -1,6 +1,6 @@ import type { ReactNode, MouseEventHandler } from 'react'; import { cn } from '@/lib/utils'; -import type { SocialCardStatus } from '@/lib/social-cards'; +import type { SocialCardStatus } from '../../lib/social-cards'; interface ModuleCardProps { children: ReactNode; diff --git a/src/components/shared/thread-drawer.tsx b/src/components/shared/thread-drawer.tsx index a5be5ec..c54a5bd 100644 --- a/src/components/shared/thread-drawer.tsx +++ b/src/components/shared/thread-drawer.tsx @@ -1,8 +1,17 @@ 'use client'; -import { X, Send } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { Edit3, MessageSquareText, Send, X } from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ScrollArea } from '@/components/ui/scroll-area'; + +import { buildEditableIssueDraft, buildIssueUpdatePayload, validateEditableIssueDraft, type EditableIssueDraft, type EditableIssueFieldErrors } from '../../lib/issue-editor'; +import type { UpdateMutationPayload } from '../../lib/mutations'; +import type { BeadIssue } from '../../lib/types'; import { ThreadView, type ThreadItem } from './thread-view'; -import { useState } from 'react'; interface ThreadDrawerProps { isOpen: boolean; @@ -10,113 +19,352 @@ interface ThreadDrawerProps { title: string; id: string; items?: ThreadItem[]; - embedded?: boolean; // New prop for embedded mode + embedded?: boolean; + issue?: BeadIssue | null; + projectRoot?: string; + onIssueUpdated?: (issueId: string) => Promise | void; } -// Sample data for demo const SAMPLE_ITEMS: ThreadItem[] = [ { id: '1', - type: 'status_change', - from: 'backlog', - to: 'in_progress', - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), + type: 'comment', + author: 'sarah.lee', + content: 'Pushed a first pass for the left rail hierarchy. Need readability check on status chips.', + timestamp: new Date(Date.now() - 6 * 60 * 1000), }, { id: '2', - type: 'comment', - author: 'zenchantlive', - content: 'Started working on this task.', - timestamp: new Date(Date.now() - 1 * 60 * 60 * 1000), + type: 'status_change', + from: 'open', + to: 'in_progress', + timestamp: new Date(Date.now() - 31 * 60 * 1000), }, { id: '3', type: 'protocol_event', event: 'HANDOFF', - content: 'Handed off to agent', - timestamp: new Date(Date.now() - 30 * 60 * 1000), + content: 'Swarm integrator picked up follow-up work.', + timestamp: new Date(Date.now() - 55 * 60 * 1000), }, ]; -export function ThreadDrawer({ isOpen, onClose, title, id, items = SAMPLE_ITEMS, embedded = false }: ThreadDrawerProps) { - const [comment, setComment] = useState(''); +const STATUS_OPTIONS: EditableIssueDraft['status'][] = ['open', 'in_progress', 'blocked', 'deferred', 'closed']; +const PRIORITY_OPTIONS = [0, 1, 2, 3, 4] as const; - if (!isOpen) return null; +async function postIssueUpdate(body: UpdateMutationPayload): Promise { + const response = await fetch('/api/beads/update', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); + + const payload = (await response.json()) as { ok: boolean; error?: { message?: string } }; + if (!response.ok || !payload.ok) { + throw new Error(payload.error?.message ?? 'Update failed'); + } +} + +function saveStateTone(state: 'ready' | 'saving' | 'saved' | 'error'): string { + if (state === 'saving') return 'border-[#5BA8A0]/50 bg-[#5BA8A0]/20 text-[#D6EEEA]'; + if (state === 'saved') return 'border-[#7CB97A]/50 bg-[#7CB97A]/20 text-[#D4ECD2]'; + if (state === 'error') return 'border-[#E24A3A]/50 bg-[#E24A3A]/20 text-[#F3C2BC]'; + return 'border-white/10 bg-white/5 text-[#B8B8B8]'; +} + +export function ThreadDrawer({ + isOpen, + onClose, + title, + id, + items = SAMPLE_ITEMS, + embedded = false, + issue, + projectRoot, + onIssueUpdated, +}: ThreadDrawerProps) { + const [comment, setComment] = useState(''); + const [editMode, setEditMode] = useState(false); + const [draft, setDraft] = useState(issue ? buildEditableIssueDraft(issue) : null); + const [fieldErrors, setFieldErrors] = useState({}); + const [saveError, setSaveError] = useState(null); + const [saveState, setSaveState] = useState<'ready' | 'saving' | 'saved' | 'error'>('ready'); + + useEffect(() => { + if (!issue) { + setDraft(null); + setEditMode(false); + setFieldErrors({}); + setSaveError(null); + setSaveState('ready'); + return; + } + + setDraft(buildEditableIssueDraft(issue)); + setEditMode(false); + setFieldErrors({}); + setSaveError(null); + setSaveState('ready'); + }, [issue]); + + const canEdit = Boolean(issue && projectRoot && draft); + + const participants = useMemo(() => { + const names = new Set(); + for (const item of items) { + if (item.author && item.author.trim()) { + names.add(item.author.trim()); + } + } + return Array.from(names).slice(0, 4); + }, [items]); + + const handleSave = async () => { + if (!issue || !projectRoot || !draft) { + return; + } + + const validation = validateEditableIssueDraft(draft); + if (!validation.ok) { + setFieldErrors(validation.errors); + setSaveState('error'); + return; + } + + const payload = buildIssueUpdatePayload(issue, draft, projectRoot); + if (!payload) { + setEditMode(false); + setSaveState('saved'); + setTimeout(() => setSaveState('ready'), 900); + return; + } + + setSaveState('saving'); + setSaveError(null); + setFieldErrors({}); + + try { + await postIssueUpdate(payload); + await onIssueUpdated?.(issue.id); + setEditMode(false); + setSaveState('saved'); + setTimeout(() => setSaveState('ready'), 900); + } catch (error) { + setSaveError(error instanceof Error ? error.message : 'Save failed'); + setSaveState('error'); + } + }; + + if (!isOpen) { + return null; + } return (
- {/* Header: Mission Control Style */} -
-
-
-
- MISSION_{id} +
+
+
+

Open Thread

+

{title}

+

{id} · {items.length} events

-

- {title} -

+ +
- -
+ - {/* Thread Content */} -
-
- + +
+
+
+
+ + Conversation +
+
+ {participants.map((name) => ( + + {name} + + ))} +
+
+ +
+ +
+
+

Task summary

+ + {saveState} + +
+ + {!issue ? ( +

No task details available for this thread context.

+ ) : !editMode ? ( +
+

{issue.title}

+

{issue.description ?? 'No description provided.'}

+
+ {issue.status} + P{issue.priority} + {issue.issue_type} + {issue.assignee ? @{issue.assignee} : null} +
+
+ +
+
+ ) : ( +
+ + {fieldErrors.title ?

{fieldErrors.title}

: null} + +