Commit Graph

13 Commits

Author SHA1 Message Date
Jiayuan Zhang
a3eefcf2c4 Revert "feat: add online status indicator on agent & member avatars (#821)" (#837)
This reverts commit 1d64ea4ba6.
2026-04-13 15:03:31 +08:00
Jiayuan Zhang
1d64ea4ba6 feat: add online status indicator on agent & member avatars (#821)
* feat: add online status indicator dot on agent & member avatars

Backend:
- Track member presence via WebSocket connections in the Hub
- Broadcast member:online/offline events when users connect/disconnect
- Add GET /api/workspaces/{id}/members/online endpoint
- Add member:online and member:offline event type constants

Frontend:
- Add isOnline prop to ActorAvatar with a status dot at top-right corner
- Green dot = online, gray dot = offline, no dot = status unknown
- Fetch online member list via new query, update optimistically on WS events
- Derive agent online status from existing agent.status field
- Wire online status through ActorAvatar views wrapper (enabled by default)

* fix: address code review — fix hub tests and avatar rounding

1. Hub tests: consume the member:online presence event from the first
   connection before asserting on broadcast messages.
2. ActorAvatar: use rounded-[inherit] on the inner wrapper so callers
   can override rounding (e.g. rounded-lg for agent list items).

* fix: consume member:online presence event in integration test

Same fix as the hub unit tests — read and discard the member:online
event before asserting on issue:created in TestWebSocketIntegration.
2026-04-13 14:46:34 +08:00
Naiyuan Qing
2fd344511e fix(realtime): add staleTime: 0 to fetchQuery in WS deleted/removed handlers
workspace:deleted and member:removed handlers were calling fetchQuery
without staleTime: 0. With staleTime: Infinity on the QueryClient, this
returns the cached list (which still contains the deleted/left workspace)
instead of fetching fresh data — so hydrateWorkspace never switches away.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:20:19 +08:00
Naiyuan Qing
43466a6402 refactor: migrate workspace list from Zustand to React Query
- Remove workspaces[] from workspace store — list is server state, belongs in React Query
- Change switchWorkspace(id) → switchWorkspace(ws) — caller provides full object from Query
- Remove createWorkspace/leaveWorkspace/deleteWorkspace store actions (duplicated mutations)
- Remove refreshWorkspaces store action — replaced by qc.fetchQuery + hydrateWorkspace
- Enhance useLeaveWorkspace/useDeleteWorkspace mutations to re-select workspace when current is removed
- useCreateWorkspace mutation now switches to new workspace on success
- AuthInitializer seeds React Query cache on boot to avoid double fetch
- Realtime sync: replace refreshWorkspaces() calls with qc.fetchQuery + hydrateWorkspace
- Sidebar reads workspace list from useQuery(workspaceListOptions()) instead of Zustand
- create-workspace modal and workspace settings tab use mutations directly
- AGENTS.md: rewrite to match current monorepo architecture, pointing to CLAUDE.md

Fixes workspace rename not updating sidebar without page refresh.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 13:38:02 +08:00
LinYushen
95bfd7dd96 feat(auth): migrate auth token to HttpOnly Cookie & WebSocket Origin whitelist (#819)
* feat(auth): migrate auth token to HttpOnly cookie & implement WebSocket Origin whitelist

Security improvements from the MUL-566 audit report:

1. Auth token is now set as an HttpOnly, SameSite=Lax cookie on login,
   preventing XSS-based token theft. Cookie-based auth includes CSRF
   protection via double-submit cookie pattern. The Authorization header
   path is preserved for Electron desktop app and CLI/PAT clients.

2. WebSocket upgrader now validates the Origin header against a
   configurable allowlist (ALLOWED_ORIGINS env var), rejecting
   connections from unauthorized origins.

Backend: new auth cookie helpers, middleware reads cookie as fallback,
WS handler accepts cookie auth, Origin whitelist, logout endpoint.
Frontend: CSRF token in API headers, cookie-aware auth store and WS
client, web app opts into cookieAuth mode while desktop keeps tokens.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(auth): address PR review — Strict cookies, HMAC-bound CSRF, origin sync

1. SameSite=Lax → SameSite=Strict per spec requirement
2. CSRF token now HMAC-signed with auth token (nonce.signature format),
   preventing subdomain cookie injection attacks
3. allowedWSOrigins uses atomic.Value to eliminate data race
4. Removed magic "cookie" sentinel string in WSProvider — pass null token
   and guard with boolean check instead
5. Removed dead delete uploadHeaders["Content-Type"] in API client

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 12:13:35 +08:00
Manish Chauhan
ec586ebc25 fix(pins): scope cache by user and fix sidebar pin action (#664) 2026-04-12 15:02:20 +08:00
Jiayuan Zhang
9b62485a86 feat: add pin to sidebar for issues and projects (#653)
* feat: add pin to sidebar for issues and projects

Add per-user pinning of issues and projects to the sidebar for quick access.

- New `pinned_item` table with per-user, per-workspace scoping
- REST API: GET/POST /api/pins, DELETE /api/pins/{type}/{id}, PUT /api/pins/reorder
- Sidebar "Pinned" section between Personal and Workspace nav (hidden when empty)
- Pin/unpin actions in issue and project detail dropdown menus
- Optimistic mutations with WebSocket invalidation for real-time sync

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add drag-and-drop reordering and visible pin buttons

- Sidebar pinned items now support drag-and-drop reordering via @dnd-kit
- Add visible pin/unpin icon button in issue and project detail headers
- Add useReorderPins mutation with optimistic updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove drag handle and fix page refresh after reorder

- Remove GripVertical drag handle — whole item is now draggable, aligning
  with other sidebar elements
- Prevent link navigation after drag using wasDragged ref
- Remove onSettled invalidation from reorder mutation to prevent
  unnecessary refetch after optimistic update

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:00:25 +08:00
Naiyuan Qing
85cff15427 feat(core): standardize storage + workspace isolation for persist stores
Unify all client-side persistence through StorageAdapter and add
workspace-scoped key namespacing (${key}:${wsId}).

- createPersistStorage: bridge for Zustand persist → StorageAdapter DI
- createWorkspaceAwareStorage: dynamic namespace by current workspace
- Migrate 6 persist stores (navigation, draft, view, scope, my-issues-view, chat)
- Rehydration registry: stores auto-rehydrate on workspace switch
- clearWorkspaceStorage: cleanup on workspace delete / member removal
- Chat store: namespace keys + rehydrate on workspace switch
- Factory view stores (createIssueViewStore): auto-register for rehydration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:59:53 +08:00
Bohan Jiang
ca7ba48934 fix(agents): invalidate runtimes cache on daemon events (#624)
The Agents page never received runtime cache updates when daemons
registered or deregistered, causing the Create Agent dialog to show
"No runtime available" even when runtimes existed. This happened because
daemon events were only handled by the Runtimes page component, not
globally.

- Add daemon:register to the centralized realtime sync refresh map
- Skip daemon:heartbeat in the generic handler to avoid excessive refetches
- Invalidate runtimes on WS reconnect alongside other workspace data
- Show a loading indicator in the Create Agent dialog while runtimes load
2026-04-10 14:50:34 +08:00
LinYushen
21484e506a fix(realtime): re-subscribe WS handlers when client reconnects (#580)
subscribe/onReconnect used wsRef (a ref) with empty useCallback deps,
so the function identity never changed when the WSClient was recreated.
Consumers' effects never re-ran, leaving handlers registered on the
old (disconnected) client.

Switch to wsClient state so the callback identity updates on reconnect,
causing all useEffect consumers to re-subscribe on the new client.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:24:22 +08:00
Bohan Jiang
68e2a14ba2 feat(projects): add Project entity with full-stack CRUD support (#552)
Implements the Project concept as a higher-level grouping for issues.
Hierarchy: workspace → project → issue → sub-issue.

Backend:
- Migration 034: project table + issue.project_id FK
- sqlc queries for project CRUD
- Project handler with list/get/create/update/delete
- Issue handler updated to support project_id in create/update
- Routes at /api/projects, WebSocket event constants

Frontend (new monorepo structure):
- @multica/core: Project types, API client methods, queries/mutations,
  status config, realtime sync
- @multica/views: Projects list page, detail page (overview + issues
  tabs), project picker for issue detail panel
- apps/web: Route pages, sidebar navigation entry

All TypeScript type checks and tests pass.
2026-04-09 14:59:16 +08:00
Naiyuan Qing
4668aad039 refactor(core): remove platform coupling — StorageAdapter, sonner, barrel cleanup
P0: Replace all localStorage calls in packages/core with StorageAdapter
- Create StorageAdapter interface (getItem/setItem/removeItem)
- Auth store factory now requires storage parameter
- Workspace store factory accepts optional storage parameter
- WSProvider accepts storage prop for token retrieval
- apps/web/platform/ passes localStorage as the web implementation

P1: Remove sonner UI dependency from packages/core
- Replace toast.error() in workspace store with onError callback
- Move sonner import to apps/web/platform/workspace.ts
- Remove sonner from packages/core/package.json dependencies

P2: Delete 5 pure re-export barrel files in apps/web/features/
- features/issues/index.ts, modals/index.ts, navigation/index.ts,
  workspace/index.ts, inbox/index.ts — all had zero consumers
- features/ now only contains auth/ (web-only cookie + initializer)
  and landing/ (web-only pages)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:16:51 +08:00
Naiyuan Qing
e1e7f68330 feat: extract packages/core — Turborepo infrastructure + headless business logic
Phase 1: Monorepo infrastructure
- Add Turborepo with turbo.json pipeline (build, dev, typecheck, test)
- Update pnpm-workspace.yaml to include packages/*
- Create shared TypeScript config (packages/tsconfig)

Phase 2: Extract packages/core (zero react-dom, all-platform reuse)
- Move domain types, API client, logger, utils → packages/core/
- Move TanStack Query modules (issues, inbox, workspace, runtimes)
- Move Zustand stores (auth, workspace, issues, navigation, modals)
- Move realtime sync (WSProvider, hooks, ws-updaters)
- Refactor auth/workspace stores to factory pattern for DI
- Refactor ApiClient with onUnauthorized callback
- Refactor useWorkspaceId to React Context (WorkspaceIdProvider)
- Refactor WSProvider to accept wsUrl + store props
- Create apps/web/platform/ bridge layer (api singleton, store instances)
- Update 91 import paths across apps/web/
- Fix 3 test files for new import paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:20:00 +08:00