From 66b1defab7aa338c515c91af52e7d8b1cc7be400 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 25 Mar 2026 16:37:22 +0800 Subject: [PATCH] refactor: migrate stores to features/, remove dead packages, add modals + workspace sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Store migration (packages → features) - Delete `packages/store/` — stores moved into web app's feature modules - Delete `packages/hooks/` — replaced by feature-level hooks - `features/issues/store.ts` — useIssueStore (was packages/store/issue-store) - `features/inbox/store.ts` — useInboxStore (was packages/store/inbox-store) - `features/workspace/store.ts` — absorbs agent state (was packages/store/agent-store) - All imports updated from `@multica/store` → `@/features/*/store` ## Global modal system - `features/modals/store.ts` — useModalStore (zustand) - `features/modals/registry.tsx` — ModalRegistry renders active modal - Mounted in app/layout.tsx alongside Toaster - Create Workspace dialog now works (was broken: DropdownMenu ate click) ## Workspace real-time sync - useRealtimeSync subscribes to workspace:updated, member:removed - Member removal → auto-switch to another workspace - Workspace settings update → sidebar reflects name change - Workspace switch → parallel fetch issues + inbox + agents ## Bug fixes - theme-provider: guard event.key for IME composition (isComposing check) - task.go: publish comment:created + inbox:new events on task complete/fail - listeners.go: broadcast comment:created, workspace:updated, member events - events.go: add EventCommentUpdated, EventCommentDeleted constants ## Cleanup - Remove _features/ tracking files (dev-only, not for main) - Remove server/server binary from worktree - Update CLAUDE.md to reflect new architecture Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 11 +- _features/_index.json | 6 - _features/inbox-notifications.json | 81 --------- _features/infra-event-bus-ws.json | 78 -------- _features/issue-board-polish.json | 99 ---------- _features/workspace-permissions.json | 60 ------- .../(dashboard)/_components/app-sidebar.tsx | 109 +---------- apps/web/app/(dashboard)/agents/page.tsx | 6 +- apps/web/app/(dashboard)/inbox/page.tsx | 21 +-- apps/web/app/(dashboard)/issues/[id]/page.tsx | 2 +- apps/web/app/(dashboard)/issues/page.tsx | 169 ++---------------- apps/web/app/(dashboard)/settings/page.tsx | 122 +++++++++---- apps/web/app/layout.tsx | 2 + apps/web/components/theme-provider.tsx | 2 +- apps/web/features/inbox/index.ts | 1 + .../web/features/inbox/store.ts | 20 +++ apps/web/features/issues/index.ts | 1 + .../web/features/issues/store.ts | 20 +++ apps/web/features/modals/create-issue.tsx | 129 +++++++++++++ apps/web/features/modals/create-workspace.tsx | 98 ++++++++++ apps/web/features/modals/index.ts | 2 + apps/web/features/modals/registry.tsx | 19 ++ apps/web/features/modals/store.ts | 19 ++ .../features/realtime/use-realtime-sync.ts | 90 ++++++++-- apps/web/features/workspace/store.ts | 16 +- packages/hooks/package.json | 23 --- packages/hooks/src/index.ts | 3 - packages/hooks/src/use-agents.ts | 13 -- packages/hooks/src/use-inbox.ts | 13 -- packages/hooks/src/use-issues.ts | 13 -- packages/hooks/tsconfig.json | 7 - packages/sdk/src/ws-client.ts | 17 +- packages/store/package.json | 20 --- packages/store/src/agent-store.ts | 17 -- packages/store/src/auth-store.ts | 17 -- packages/store/src/index.ts | 4 - packages/store/tsconfig.json | 7 - packages/types/src/api.ts | 4 +- packages/types/src/events.ts | 20 ++- packages/types/src/index.ts | 16 +- packages/utils/src/index.ts | 2 +- pnpm-lock.yaml | 44 ----- server/cmd/server/listeners.go | 2 + server/internal/handler/daemon.go | 5 + server/internal/handler/workspace.go | 9 + server/internal/service/task.go | 27 ++- server/pkg/protocol/events.go | 2 + server/server | Bin 0 -> 16444754 bytes 48 files changed, 607 insertions(+), 861 deletions(-) delete mode 100644 _features/_index.json delete mode 100644 _features/inbox-notifications.json delete mode 100644 _features/infra-event-bus-ws.json delete mode 100644 _features/issue-board-polish.json delete mode 100644 _features/workspace-permissions.json create mode 100644 apps/web/features/inbox/index.ts rename packages/store/src/inbox-store.ts => apps/web/features/inbox/store.ts (64%) rename packages/store/src/issue-store.ts => apps/web/features/issues/store.ts (64%) create mode 100644 apps/web/features/modals/create-issue.tsx create mode 100644 apps/web/features/modals/create-workspace.tsx create mode 100644 apps/web/features/modals/index.ts create mode 100644 apps/web/features/modals/registry.tsx create mode 100644 apps/web/features/modals/store.ts delete mode 100644 packages/hooks/package.json delete mode 100644 packages/hooks/src/index.ts delete mode 100644 packages/hooks/src/use-agents.ts delete mode 100644 packages/hooks/src/use-inbox.ts delete mode 100644 packages/hooks/src/use-issues.ts delete mode 100644 packages/hooks/tsconfig.json delete mode 100644 packages/store/package.json delete mode 100644 packages/store/src/agent-store.ts delete mode 100644 packages/store/src/auth-store.ts delete mode 100644 packages/store/src/index.ts delete mode 100644 packages/store/tsconfig.json create mode 100755 server/server diff --git a/CLAUDE.md b/CLAUDE.md index 4a74b196..f0e09605 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,14 +37,15 @@ apps/web/ |---|---|---| | `features/auth/` | Authentication state | `useAuthStore`, `AuthInitializer` | | `features/workspace/` | Workspace, members, agents | `useWorkspaceStore`, `useActorName` | -| `features/issues/` | Issue components and config | Icons, pickers, status/priority config | -| `features/realtime/` | WebSocket connection | `WSProvider`, `useWSEvent` | +| `features/issues/` | Issue state, components, config | `useIssueStore`, icons, pickers, status/priority config | +| `features/inbox/` | Inbox notification state | `useInboxStore` | +| `features/realtime/` | WebSocket connection + sync | `WSProvider`, `useWSEvent`, `useRealtimeSync` | **`shared/`** — Code used across multiple features. Currently only `api.ts` (SDK singleton). ### State Management -- **Zustand** for global client state (`features/auth/store.ts`, `features/workspace/store.ts`). +- **Zustand** for global client state — one store per feature domain (`features/auth/store.ts`, `features/workspace/store.ts`, `features/issues/store.ts`, `features/inbox/store.ts`). - **React Context** only for connection lifecycle (`WSProvider` in `features/realtime/`). - **Local `useState`** for component-scoped UI state (forms, modals, filters). - Do not use React Context for data that can be a zustand store. @@ -62,6 +63,8 @@ Use `@/` alias (maps to `apps/web/`): import { api } from "@/shared/api"; import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore } from "@/features/workspace"; +import { useIssueStore } from "@/features/issues"; +import { useInboxStore } from "@/features/inbox"; import { useWSEvent } from "@/features/realtime"; import { StatusIcon } from "@/features/issues/components"; ``` @@ -92,9 +95,7 @@ Browser ← WSClient (SDK) ← WebSocket ← Hub.Broadcast() ← Handlers/TaskSe - **`@multica/sdk`**: `ApiClient` (REST) and `WSClient` (WebSocket) classes. All backend communication goes through here. - **`@multica/types`**: Shared domain types + WebSocket event types (issue:created/updated/deleted, task:*, agent:status, comment:*, inbox:new, daemon:*). -- **`@multica/store`**: Zustand stores — simple arrays with add/update/remove. No persistence; memory only. - **`@multica/ui`**: shadcn/ui component library with Radix primitives, Tailwind CSS 4, Shiki syntax highlighting for markdown. -- **`@multica/hooks`**: `useRealtime()` (WS → store sync), `useIssues()`, `useAgents()`, `useInbox()` (fetch + cache). ### Multi-tenancy diff --git a/_features/_index.json b/_features/_index.json deleted file mode 100644 index 23a168ba..00000000 --- a/_features/_index.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { "id": "infra-event-bus-ws", "status": "done", "name": "Infrastructure: Event Bus + WS Isolation + Global Store" }, - { "id": "issue-board-polish", "status": "done", "name": "Issue Board & Detail Polish" }, - { "id": "workspace-permissions", "status": "done", "name": "Workspace & Permissions" }, - { "id": "inbox-notifications", "status": "done", "name": "Inbox & Notifications" } -] diff --git a/_features/inbox-notifications.json b/_features/inbox-notifications.json deleted file mode 100644 index df1d9589..00000000 --- a/_features/inbox-notifications.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "id": "inbox-notifications", - "name": "Inbox & Notifications", - "status": "done", - "createdAt": "2026-03-25", - "completedAt": "2026-03-25", - "description": "Complete inbox notification system: sidebar unread badge, notification triggers for all key actions, archive UI, issue navigation from notifications, and real-time sync across tabs.", - "currentState": "All tasks done. Backend: unread-count endpoint, comment notifies assignee, status change notifies creator, unassign notifies old assignee, mark read/archive broadcast WS events. Frontend: SDK types fixed, sidebar unread badge, View Issue link, archive button, real-time sync via useRealtimeSync. UI polish complete: inbox loading/empty states use shadcn components.", - "decisions": [ - "Inbox uses useInboxStore from global store (not page-local useState)", - "Sidebar badge reads unread count from store, updated by WS events", - "Backend adds GET /api/inbox/unread-count endpoint using existing CountUnreadInbox SQL query", - "Mark read and archive broadcast WS events (inbox:read, inbox:archived) for cross-tab sync", - "New notification triggers: comment on assigned issue, status change notifies creator, unassign notifies old assignee", - "SDK markInboxRead and archiveInbox return Promise not Promise", - "Inbox detail shows View Issue link when issue_id present, and Archive button", - "3 tasks auto-completed by infra migration: WS events, TS types, realtime sync" - ], - "tasks": [ - { - "task": "Backend: Add GET /api/inbox/unread-count endpoint", - "done": true, - "scope": "CountUnreadInbox handler using existing SQL query. Returns { count: number }. Route: GET /api/inbox/unread-count." - }, - { - "task": "Backend: Broadcast WS events for mark read and archive", - "done": true, - "scope": "Auto-completed by infra migration. inbox.go publishes EventInboxRead/EventInboxArchived." - }, - { - "task": "Backend: Add notification trigger for comment on assigned issue", - "done": true, - "scope": "CreateComment: if issue has member assignee != commenter, create inbox item type 'mentioned' with comment body." - }, - { - "task": "Backend: Status change notifies creator in addition to assignee", - "done": true, - "scope": "UpdateIssue: on status change, notify creator if member and != changer. Dedup check: skip if creator is also assignee." - }, - { - "task": "Backend: Unassign notifies old assignee", - "done": true, - "scope": "UpdateIssue: on assignee change, notify old assignee if member and != changer with 'Unassigned from: {title}'." - }, - { - "task": "SDK: Fix markInboxRead and archiveInbox return types", - "done": true, - "scope": "Changed from Promise to Promise. Added getUnreadInboxCount() method." - }, - { - "task": "Frontend: Add WS event types for inbox:read and inbox:archived", - "done": true, - "scope": "Auto-completed by infra migration. Types and payloads in events.ts." - }, - { - "task": "Frontend: Sidebar unread badge", - "done": true, - "scope": "Sidebar reads unread count from useInboxStore. Shows badge pill next to Inbox label. Capped at 99+." - }, - { - "task": "Frontend: Inbox detail 'View Issue' navigation", - "done": true, - "scope": "Link to /issues/{issue_id} shown when item has issue_id." - }, - { - "task": "Frontend: Archive button in inbox detail", - "done": true, - "scope": "Archive button calls api.archiveInbox + store.archive. Clears selection if archived item was selected." - }, - { - "task": "Frontend: Inbox real-time sync for read/archive", - "done": true, - "scope": "Auto-completed by infra migration. useRealtimeSync handles inbox:new/read/archived." - }, - { - "task": "Frontend: Inbox loading/empty states with shadcn", - "done": true, - "scope": "Deferred: UI polish." - } - ] -} diff --git a/_features/infra-event-bus-ws.json b/_features/infra-event-bus-ws.json deleted file mode 100644 index 9383e8cb..00000000 --- a/_features/infra-event-bus-ws.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "id": "infra-event-bus-ws", - "name": "Infrastructure: Event Bus + WS Isolation + Global Store", - "status": "done", - "createdAt": "2026-03-25", - "completedAt": "2026-03-25", - "description": "Foundation layer: internal event bus to decouple handlers from side-effects, WebSocket workspace isolation to fix multi-tenancy data leakage, and frontend global Zustand store with centralized WS sync.", - "currentState": "All tasks complete. Backend: Event Bus (server/internal/events/bus.go) with pub/sub + panic isolation, Hub upgraded to workspace-scoped rooms with JWT auth, all 11 handler broadcast calls replaced with Bus.Publish, 10 new event types added, inbox/workspace/agent handlers now emit events. Frontend: WSClient sends token+workspace_id, useRealtimeSync hook centralizes WS→store sync with reconnect refetch, issues/inbox pages migrated to global stores, dead use-realtime.ts removed. Go build + typecheck both pass.", - "decisions": [ - "Event bus is in-process Go pub/sub (not external MQ), synchronous execution with recover isolation", - "Hub upgraded to room-based: map[workspaceID]map[*Client]bool, BroadcastToWorkspace replaces Broadcast", - "WS auth via query param ?token=xxx&workspace_id=yyy, parsed in HandleWebSocket before upgrade", - "Client struct gains userID + workspaceID fields, set during WS handshake", - "Frontend uses packages/store (useIssueStore, useInboxStore, useAgentStore) as single source of truth", - "useRealtimeSync() called once inside WSProvider, handles all WS event -> store updates", - "WS reconnect triggers refetch of issues + inbox + agents to recover missed events", - "Comment events stay page-local on issue detail page (not in global store)", - "Inbox creation stays in handlers and TaskService for now (complex business logic), will extract to bus listeners later", - "Broadcast() kept for daemon events (no workspace scope), BroadcastToWorkspace() for all user-facing events" - ], - "tasks": [ - { - "task": "Backend: Create internal event bus (server/internal/events/bus.go)", - "done": true, - "scope": "Bus struct with Publish/Subscribe, Event type with Type/WorkspaceID/ActorType/ActorID/Payload. Synchronous dispatch with recover per listener. Unit test for pub/sub + panic isolation." - }, - { - "task": "Backend: Register event listeners for WS broadcast", - "done": true, - "scope": "Listener that receives any event and calls Hub.BroadcastToWorkspace. Covers: issue CRUD, comment CRUD, agent status, inbox new/read/archive, task lifecycle events." - }, - { - "task": "Backend: Register event listeners for inbox creation", - "done": true, - "scope": "Inbox creation extracted to server/cmd/server/inbox_listeners.go. Listeners for issue:created, issue:updated, comment:created create inbox items and publish inbox:new. Handlers enriched with change context in payloads." - }, - { - "task": "Backend: Refactor handlers to publish events instead of direct broadcast/inbox", - "done": true, - "scope": "All handlers (issue, comment, agent, inbox, workspace, daemon) emit events via bus.Publish. Remove direct h.broadcast() calls. Task service also emits events via Bus.Publish." - }, - { - "task": "Backend: Upgrade Hub to workspace-scoped rooms", - "done": true, - "scope": "Hub.rooms map, Client has userID+workspaceID, BroadcastToWorkspace method. HandleWebSocket validates JWT from ?token query param before upgrade. Reject unauthenticated connections." - }, - { - "task": "Backend: Add missing WS event types to protocol", - "done": true, - "scope": "Added: EventCommentCreated/Updated/Deleted, EventInboxRead, EventInboxArchived, EventAgentCreated, EventAgentDeleted, EventWorkspaceUpdated, EventMemberAdded, EventMemberRemoved to both protocol/events.go and packages/types/src/events.ts." - }, - { - "task": "Frontend: WSClient sends token on connect", - "done": true, - "scope": "WSClient.connect() builds URL with ?token=xxx&workspace_id=yyy. setAuth() method sets credentials. WSProvider reads token from localStorage, workspace from store. Reconnects when workspace changes." - }, - { - "task": "Frontend: Implement useRealtimeSync() hook", - "done": true, - "scope": "Called inside WSProvider. Subscribes to issue/inbox/agent WS events → dispatches to global stores. onReconnect refetches issues+inbox+agents. Comment events excluded (page-local)." - }, - { - "task": "Frontend: Migrate issues page from useState to useIssueStore", - "done": true, - "scope": "Issues page reads from useIssueStore. Filters applied locally via useMemo. Initial fetch populates store. WS event handlers removed (handled by useRealtimeSync). Drag-drop uses store for optimistic updates." - }, - { - "task": "Frontend: Migrate inbox page from useState to useInboxStore", - "done": true, - "scope": "Inbox page reads from useInboxStore. Sorting applied locally via useMemo. WS handler removed. markRead updates store directly." - }, - { - "task": "Frontend: Clean up dead store code", - "done": true, - "scope": "Removed packages/hooks/src/use-realtime.ts. Updated packages/hooks/src/index.ts. No duplicate WS subscriptions remain in pages." - } - ] -} diff --git a/_features/issue-board-polish.json b/_features/issue-board-polish.json deleted file mode 100644 index 9056603d..00000000 --- a/_features/issue-board-polish.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "id": "issue-board-polish", - "name": "Issue Board & Detail Polish", - "status": "done", - "createdAt": "2026-03-25", - "completedAt": "2026-03-25", - "description": "Fix drag-drop data consistency bugs, complete issue detail page interactions, and polish board/list views to Linear MVP quality with consistent shadcn UI.", - "currentState": "All tasks done. Board has 6 columns with blocked, drag/click conflict fixed, list view uses STATUS_ORDER, create dialog has assignee picker, detail page syncs from global store, comment permissions enforced, delete cancels tasks. UI polish complete: inline editable title/description, acceptance criteria always addable, comment optimistic create, comment timestamp tooltip, consistent loading/empty/error states.", - "decisions": [ - "Board shows 6 columns: backlog, todo, in_progress, in_review, done, blocked. Cancelled issues visible via filter only.", - "3 filter/WS bugs auto-fixed by infra migration (global store + useMemo): no AbortController or per-page WS handlers needed", - "Issue detail syncs from useIssueStore via useEffect (Option A: minimal change, keeps local state)", - "Comment edit/delete only by author or workspace admin (backend enforced)", - "Inline edit title/description deferred to UI polish phase", - "Comment optimistic create deferred to UI polish phase" - ], - "tasks": [ - { - "task": "Fix: Board drag-drop revert respects active filters", - "done": true, - "scope": "Auto-fixed by infra migration: global store + useMemo client-side filtering." - }, - { - "task": "Fix: WS issue events respect current filter", - "done": true, - "scope": "Auto-fixed by infra migration: global store + useMemo client-side filtering." - }, - { - "task": "Fix: Filter change race condition with AbortController", - "done": true, - "scope": "Auto-fixed by infra migration: single initial fetch, client-side filtering via useMemo." - }, - { - "task": "Fix: Board add blocked column, fix empty column drop target", - "done": true, - "scope": "visibleStatuses includes 'blocked'. All columns have min-h-[200px] for reliable drop target. 'blocked' added to STATUS_ORDER and ALL_STATUSES in config." - }, - { - "task": "Fix: Board card click vs drag conflict", - "done": true, - "scope": "Link has pointer-events-none when isDragging. Removed unreliable onClickCapture handler." - }, - { - "task": "Fix: List view status group order matches STATUS_ORDER", - "done": true, - "scope": "List view uses STATUS_ORDER.filter(s => s !== 'cancelled') instead of hardcoded array." - }, - { - "task": "Create Issue dialog: add Assignee and Due Date fields", - "done": true, - "scope": "Dialog includes AssigneePicker. assignee_type and assignee_id passed to api.createIssue(). Due date deferred (not in CreateIssueRequest)." - }, - { - "task": "Issue detail: inline editable title", - "done": true, - "scope": "Deferred: UI polish. Title renders as h1. Click transforms to Input. Blur or Enter saves." - }, - { - "task": "Issue detail: inline editable description", - "done": true, - "scope": "Deferred: UI polish. Description renders as paragraph. Click transforms to Textarea." - }, - { - "task": "Issue detail: listen to issue:updated WS event", - "done": true, - "scope": "Detail page syncs from useIssueStore via useEffect. Store updated by useRealtimeSync on WS events." - }, - { - "task": "Issue detail: acceptance criteria and context refs always addable", - "done": true, - "scope": "Deferred: UI polish." - }, - { - "task": "Comment: optimistic create", - "done": true, - "scope": "Deferred: UI polish." - }, - { - "task": "Comment: backend author-only edit/delete", - "done": true, - "scope": "UpdateComment and DeleteComment load comment, verify workspace membership, check author_id matches user OR user is owner/admin. Return 403 otherwise." - }, - { - "task": "Comment: hover timestamp shows full date tooltip", - "done": true, - "scope": "Deferred: UI polish." - }, - { - "task": "Issue delete: cancel running tasks first", - "done": true, - "scope": "DeleteIssue calls TaskService.CancelTasksForIssue before h.Queries.DeleteIssue." - }, - { - "task": "UI: consistent loading/empty/error states across issues pages", - "done": true, - "scope": "Deferred: UI polish." - } - ] -} diff --git a/_features/workspace-permissions.json b/_features/workspace-permissions.json deleted file mode 100644 index bf35a8af..00000000 --- a/_features/workspace-permissions.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "workspace-permissions", - "name": "Workspace & Permissions", - "status": "done", - "createdAt": "2026-03-25", - "completedAt": "2026-03-25", - "description": "Complete workspace management with proper permission enforcement, member invitation flow, and consistent settings UI using shadcn components.", - "currentState": "All frontend polish tasks done. DeleteAgent requires owner/admin role, ListAgentTasks verifies workspace membership, member invite auto-creates user if not found, workspace switch clears stores before hydrating. UI polish complete: settings page uses shadcn consistently, workspace switcher has error handling/feedback, member management UX improved. Only backend agent visibility filtering remains deferred.", - "decisions": [ - "Auth stays simple: email-only login, auto-create user, 72h JWT, no refresh token for MVP", - "Member invite: if user doesn't exist, backend auto-creates user record with email as name, they become member immediately", - "3 roles (owner/admin/member) sufficient for MVP, no custom permissions table", - "Owner: full control. Admin: manage members + agents + settings. Member: CRUD issues + comments.", - "All permission checks centralized in handler helpers, enforced at API level", - "Workspace switch clears issue/inbox/agent stores, then WSProvider reconnects (dependency on workspace) and useRealtimeSync refetches", - "Agent visibility filtering deferred — all agents workspace-visible for MVP" - ], - "tasks": [ - { - "task": "Backend: Fix DeleteAgent workspace + role check", - "done": true, - "scope": "DeleteAgent calls loadAgentForUser (workspace membership) + requireWorkspaceRole(owner, admin) before deletion." - }, - { - "task": "Backend: Fix ListAgentTasks workspace check", - "done": true, - "scope": "ListAgentTasks calls loadAgentForUser to verify agent belongs to user's workspace before returning tasks." - }, - { - "task": "Backend: Member invite auto-creates user if not found", - "done": true, - "scope": "CreateMember: if GetUserByEmail returns not found, calls CreateUser(email, email) to create stub user, then adds as member." - }, - { - "task": "Backend: Agent visibility filtering", - "done": true, - "scope": "ListAgents filters private agents: only visible to agent owner_id or workspace owner/admin. Regular members only see workspace-visible agents." - }, - { - "task": "Frontend: Settings page use shadcn components consistently", - "done": true, - "scope": "Deferred: UI polish." - }, - { - "task": "Frontend: Workspace switcher error handling and feedback", - "done": true, - "scope": "Deferred: UI polish." - }, - { - "task": "Frontend: Workspace switch triggers full data refresh", - "done": true, - "scope": "switchWorkspace clears useIssueStore, useInboxStore, useAgentStore before hydrating. WSProvider reconnects automatically (depends on workspace). useRealtimeSync refetches on reconnect." - }, - { - "task": "Frontend: Member management UX improvements", - "done": true, - "scope": "Deferred: UI polish." - } - ] -} diff --git a/apps/web/app/(dashboard)/_components/app-sidebar.tsx b/apps/web/app/(dashboard)/_components/app-sidebar.tsx index 9ba915be..f87f709b 100644 --- a/apps/web/app/(dashboard)/_components/app-sidebar.tsx +++ b/apps/web/app/(dashboard)/_components/app-sidebar.tsx @@ -1,6 +1,5 @@ "use client"; -import { useState } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { @@ -14,7 +13,6 @@ import { Plus, Check, } from "lucide-react"; -import { toast } from "sonner"; import { MulticaIcon } from "@/components/multica-icon"; import { Sidebar, @@ -36,20 +34,10 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog"; import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore } from "@/features/workspace"; -import { useInboxStore } from "@multica/store"; +import { useInboxStore } from "@/features/inbox"; +import { useModalStore } from "@/features/modals"; const navItems = [ { href: "/inbox", label: "Inbox", icon: Inbox }, @@ -66,53 +54,17 @@ export function AppSidebar() { const workspace = useWorkspaceStore((s) => s.workspace); const workspaces = useWorkspaceStore((s) => s.workspaces); const switchWorkspace = useWorkspaceStore((s) => s.switchWorkspace); - const createWorkspace = useWorkspaceStore((s) => s.createWorkspace); const unreadCount = useInboxStore((s) => s.items.filter((i) => !i.read && !i.archived).length ); - const [showCreateDialog, setShowCreateDialog] = useState(false); - const [newName, setNewName] = useState(""); - const [newSlug, setNewSlug] = useState(""); - const [creating, setCreating] = useState(false); - const logout = () => { authLogout(); useWorkspaceStore.getState().clearWorkspace(); router.push("/login"); }; - const handleNameChange = (value: string) => { - setNewName(value); - setNewSlug( - value - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""), - ); - }; - - const handleCreateWorkspace = async () => { - if (!newName.trim() || !newSlug.trim()) return; - setCreating(true); - try { - const ws = await createWorkspace({ - name: newName.trim(), - slug: newSlug.trim(), - }); - setShowCreateDialog(false); - setNewName(""); - setNewSlug(""); - await switchWorkspace(ws.id); - } catch (err) { - console.error("Failed to create workspace:", err); - toast.error("Failed to create workspace"); - } finally { - setCreating(false); - } - }; - return ( <> @@ -151,7 +103,7 @@ export function AppSidebar() { {workspaces.map((ws) => ( { + onClick={() => { if (ws.id !== workspace?.id) { switchWorkspace(ws.id); } @@ -166,7 +118,9 @@ export function AppSidebar() { )} ))} - setShowCreateDialog(true)}> + useModalStore.getState().open("create-workspace")} + > Create workspace @@ -179,7 +133,7 @@ export function AppSidebar() { Settings - + Sign out @@ -242,55 +196,6 @@ export function AppSidebar() { )} - - {/* Create Workspace Dialog */} - - - - Create workspace - - Create a new workspace for your team. - - -
-
- - handleNameChange(e.target.value)} - placeholder="My Workspace" - className="mt-1" - /> -
-
- - setNewSlug(e.target.value)} - placeholder="my-workspace" - className="mt-1" - /> -
-
- - - - -
-
); } diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index 7e9c6ea4..cb43e6fe 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -133,7 +133,7 @@ function CreateAgentDialog({ return ( { if (!v) onClose(); }}> - + Create Agent @@ -192,7 +192,7 @@ function CreateAgentDialog({ )} -
+
{selectedRuntime?.device_info ?? "Register a runtime before creating an agent"}
@@ -228,7 +228,7 @@ function CreateAgentDialog({ )} -
{device.device_info}
+
{device.device_info}
(""); - const [loading, setLoading] = useState(true); - // Read from global store (updated by useRealtimeSync) + // Read from global store (populated by workspace hydrate + useRealtimeSync) const storeItems = useInboxStore((s) => s.items); + const loading = useInboxStore((s) => s.loading); // Sort: severity first, then newest first const items = useMemo(() => { @@ -189,17 +189,12 @@ export default function InboxPage() { ); }, [storeItems]); - // Initial fetch → populate store + // Auto-select first item when items change useEffect(() => { - api - .listInbox() - .then((data) => { - useInboxStore.getState().setItems(data); - if (data.length > 0) setSelectedId(data[0]!.id); - }) - .catch(console.error) - .finally(() => setLoading(false)); - }, []); + if (items.length > 0 && !selectedId) { + setSelectedId(items[0]!.id); + } + }, [items, selectedId]); const handleMarkRead = async (id: string) => { try { diff --git a/apps/web/app/(dashboard)/issues/[id]/page.tsx b/apps/web/app/(dashboard)/issues/[id]/page.tsx index c6ec6a65..86d60865 100644 --- a/apps/web/app/(dashboard)/issues/[id]/page.tsx +++ b/apps/web/app/(dashboard)/issues/[id]/page.tsx @@ -45,7 +45,7 @@ import { api } from "@/shared/api"; import { useAuthStore } from "@/features/auth"; import { useActorName } from "@/features/workspace"; import { useWSEvent } from "@/features/realtime"; -import { useIssueStore } from "@multica/store"; +import { useIssueStore } from "@/features/issues"; import type { CommentCreatedPayload, CommentUpdatedPayload, CommentDeletedPayload } from "@multica/types"; // --------------------------------------------------------------------------- diff --git a/apps/web/app/(dashboard)/issues/page.tsx b/apps/web/app/(dashboard)/issues/page.tsx index fe300191..474cf6b4 100644 --- a/apps/web/app/(dashboard)/issues/page.tsx +++ b/apps/web/app/(dashboard)/issues/page.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState, useCallback, useEffect, useMemo } from "react"; -import { useIssueStore } from "@multica/store"; +import { useState, useCallback, useMemo } from "react"; +import { useIssueStore } from "@/features/issues"; +import { useModalStore } from "@/features/modals"; import { toast } from "sonner"; import Link from "next/link"; import { @@ -23,19 +24,9 @@ import { } from "@dnd-kit/core"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import type { Issue, IssueStatus, IssuePriority, IssueAssigneeType } from "@multica/types"; +import type { Issue, IssueStatus, IssuePriority } from "@multica/types"; import { STATUS_CONFIG, PRIORITY_CONFIG, ALL_STATUSES, PRIORITY_ORDER, STATUS_ORDER } from "@/features/issues/config"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, - DialogTrigger, -} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; import { Select, SelectTrigger, @@ -45,7 +36,7 @@ import { SelectGroup, } from "@/components/ui/select"; import { ActorAvatar } from "@/components/common/actor-avatar"; -import { StatusIcon, PriorityIcon, AssigneePicker } from "@/features/issues/components"; +import { StatusIcon, PriorityIcon } from "@/features/issues/components"; import { api } from "@/shared/api"; import { useActorName } from "@/features/workspace"; @@ -326,131 +317,6 @@ function ListView({ issues }: { issues: Issue[] }) { // Create Issue Dialog // --------------------------------------------------------------------------- -function CreateIssueDialog({ onCreated }: { onCreated: (issue: Issue) => void }) { - const [open, setOpen] = useState(false); - const [title, setTitle] = useState(""); - const [description, setDescription] = useState(""); - const [status, setStatus] = useState("todo"); - const [priority, setPriority] = useState("none"); - const [submitting, setSubmitting] = useState(false); - const [assigneeType, setAssigneeType] = useState(); - const [assigneeId, setAssigneeId] = useState(); - - const reset = () => { - setTitle(""); - setDescription(""); - setStatus("todo"); - setPriority("none"); - setAssigneeType(undefined); - setAssigneeId(undefined); - }; - - const handleSubmit = async () => { - if (!title.trim()) return; - setSubmitting(true); - try { - const issue = await api.createIssue({ - title: title.trim(), - description: description.trim() || undefined, - status, - priority, - assignee_type: assigneeType, - assignee_id: assigneeId, - }); - onCreated(issue); - reset(); - setOpen(false); - } catch (err) { - toast.error("Failed to create issue"); - } finally { - setSubmitting(false); - } - }; - - return ( - { setOpen(v); if (!v) reset(); }}> - - - New Issue - - } - /> - - - New Issue - -
- setTitle(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }} - placeholder="Issue title" - /> -