- Refactor store to persist raw user intent (chatWidth/chatHeight/isExpanded) with no clamp logic
- Add ResizeObserver-based resize hook for dynamic container tracking
- Add drag-to-resize handles (left, top, corner) with pointer capture
- Expand/Restore button uses visual state (isAtMax) not internal flag
- Open/close animation (scale + opacity from bottom-right)
- Resize animation on button click, instant on drag (isDragging gate)
- Move ChatWindow inside content area (absolute, not fixed)
- Add input draft persistence, remove agent prop from message list
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The server's WS origin whitelist (added in #819) rejects connections
from localhost dev origins. Desktop app doesn't need Origin-based
security since it runs in Electron with webSecurity disabled.
Strip the Origin header from WS upgrade requests in the main process
so the server's checkOrigin allows the connection.
Desktop Google login flow: click "Continue with Google" → opens default
browser to web login page with platform=desktop → Google OAuth completes
→ web callback redirects to multica://auth/callback?token=<jwt> →
Electron receives deep link, extracts token, completes login.
Changes:
- Register `multica://` protocol in Electron (main process + builder)
- Add single-instance lock with deep link forwarding (macOS + Win/Linux)
- Expose `desktopAPI.onAuthToken` and `openExternal` via preload IPC
- Add `loginWithToken(token)` to core auth store
- Pass `state=platform:desktop` through Google OAuth flow
- Web callback detects desktop state and redirects via deep link
- Desktop renderer listens for auth token and hydrates session
The logout handler was clearing `multica_workspace_id` from storage,
so re-login always defaulted to the first workspace. The workspace ID
is a user preference, not session-sensitive data — keep it so both
web and desktop restore the correct workspace after re-authentication.
Also pass `lastWorkspaceId` in the desktop login page, which was
previously missing.
- Set webSecurity: false in BrowserWindow to bypass CORS when
connecting to remote API (standard Electron practice)
- Fix renderer dev server to port 5173 so localStorage persists
across restarts (prevents losing login state)
Adds horizontal drag-and-drop reordering for the desktop tab bar using
@dnd-kit/sortable, with axis + parent constraints so tabs only slide
horizontally within the bar. Order is persisted automatically through
the existing tab-store partialize.
Also brings tab-store into the standardized storage pipeline introduced
in 85cff154 — it was the last persist store still using vanilla zustand
persist instead of createPersistStorage(defaultStorage). Storage key
multica_tabs is unchanged so existing user data is preserved.
- apps/desktop: add @dnd-kit/{core,sortable,modifiers,utilities}
- tab-store: moveTab(from, to) action via arrayMove (preserves router refs)
- tab-store: persist storage → createJSONStorage(createPersistStorage(defaultStorage))
- tab-bar: DndContext + SortableContext(horizontalListSortingStrategy)
- tab-bar: restrictToHorizontalAxis + restrictToParentElement modifiers
- tab-bar: PointerSensor distance:5 to disambiguate click vs drag
- tab-bar: stopPropagation on close-button pointerdown to avoid drag start
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Desktop app was missing Geist font — the CSS variable `--font-sans` referenced
by `@theme inline` in tokens.css was never defined, causing fallback to the
Chromium default system font. Web app worked because Next.js `next/font/google`
injected the variable.
Fix: add @fontsource/geist-sans and @fontsource/geist-mono, import the font
CSS in main.tsx, and define --font-sans/--font-mono in globals.css.
Closes MUL-504
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move WebkitAppRegion="no-drag" from the tab bar container to individual
buttons (TabItem and NewTabButton). This lets the empty space between
tabs remain part of the window drag region while still making the tabs
themselves clickable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move chat queries, mutations, and store from apps/web/core/chat/ and
apps/web/features/chat/store.ts to packages/core/chat/. Refactor store
to use createChatStore({ storage }) factory pattern (mirrors auth store)
so it works in both web (localStorage) and desktop (Electron) without
direct browser API access. Register chat store in CoreProvider.initCore.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevent showing the X button on hover for the last tab, since closing
it just replaces with a default tab — misleading UX.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add Zustand persist middleware to tab store so open tabs survive app
restarts. Uses merge callback to rebuild memory routers from persisted
paths on rehydration. History stacks start fresh (matches browser
"restore tabs" behavior).
- partialize: strips router/historyIndex/historyLength (not serializable)
- merge: recreates routers via createTabRouter(path), validates activeTabId
- version: 1 for future migration support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire /projects/:id in desktop router with ProjectDetailPage wrapper
(dynamic document title). Add FolderKanban icon mapping for project
tabs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DesktopLayout → DesktopShell, AppContent handles auth routing at top
level, tab-bar and tab-sync adapted for per-tab memory routers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each tab gets its own createMemoryRouter instance. React Activity API
preserves DOM and React state for hidden tabs. Navigation adapters
split into root-level (sidebar/modals) and per-tab providers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both web and desktop had independent guard + WorkspaceIdProvider logic.
Extract into a single DashboardGuard component so future changes only
need one update.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moved SearchCommand, SearchTrigger, and search store from apps/web/features/
to packages/views/search/. Replaced useRouter (next/navigation) with the
existing useNavigation() abstraction. Wired search into desktop layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @multica/eslint-config package (base, react, next configs)
- Replace `next lint` (removed in Next.js 16) with `eslint .`
- Add lint scripts to all packages and desktop app
- Add noUnusedLocals, noUnusedParameters, noImplicitReturns to base tsconfig
- Fix all resulting TS/ESLint errors (unused imports, missing returns,
stale eslint-disable comments from legacy eslint-config-next)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tab system:
- Tab store with open/add/close/switch actions
- document.title as single source of truth for tab titles (MutationObserver)
- Route-level default titles via react-router handle.title + TitleSync
- useDocumentTitle hook for dynamic titles (e.g. issue detail)
- Tab bar with fixed-width tabs, fade mask, hover-to-close
Login upgrade:
- Upgrade shared LoginPage with InputOTP, cooldown resend, Google OAuth support
- Google OAuth controlled via optional google prop (desktop omits it)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract MulticaIcon and ThemeProvider to packages/ui (remove duplication)
- Extract shared CSS (scrollbar, shiki, entrance-spin) to packages/ui/styles/base.css
- Add NavigationAdapter.openInNewTab/getShareableUrl for platform-agnostic navigation
- Fix window.open() / window.location.href in shared views to use NavigationAdapter
- Add resolve.dedupe for React in electron-vite config
- Fix desktop tsconfig (noImplicitAny: true)
- Use catalog: for all desktop dependencies
- Add shadcn + tw-animate-css to desktop dependencies (fix phantom deps)
- Add typecheck scripts to all shared packages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
base-ui Button defaults to type="button", which doesn't trigger form
onSubmit. Explicit type="submit" fixes the click-to-submit flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: "type": "module" made Node.js treat all .js as ESM, but
Electron loads preload via require() (CJS). Removing it makes .js
default to CJS, which is what Electron expects. No rollup overrides needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Preload output as .cjs so Node.js treats it as CJS regardless of
"type": "module" in package.json
- Add electron-vite dev server ports (5173, 5174) to default CORS origins
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Preload must be CJS (Electron loads it via require), force format: "cjs"
and entryFileNames: "[name].js" so output matches main's reference
- @source paths were 4 levels up but need 5 (src/renderer/src/ to root)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Electron renderer IS a browser — localStorage works natively, no need
for electron-store in preload. Removes the preload module loading issue
and eliminates an unnecessary dependency + IPC bridge.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AuthInitializer was inside DashboardShell which has an isLoading early
return — the initializer never rendered, so isLoading never became false.
Moved to App.tsx (same as web's root layout) so it always executes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- multica-icon: copied from web, zero platform-specific deps
- theme-provider: next-themes + TooltipProvider wrapper
- title-bar: draggable frameless title bar with macOS traffic light inset
- app-sidebar: adapted from web — uses @multica/views/navigation instead of next/link
- dashboard-shell: root layout with auth guard, sidebar, outlet, and workspace provider
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On macOS, calling win.hide() on a fullscreen window causes a black screen
because it interrupts the Space transition animation. This is a known
macOS Cocoa limitation (NSWindow.orderOut() + fullscreen).
Fix: Exit fullscreen first and wait for 'leave-full-screen' event before
hiding the window.
Also added render-process-gone handler for debugging renderer crashes.
Refs:
- https://github.com/electron/electron/issues/20263
- https://github.com/electron/electron/issues/6033
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
dev-app-update.yml and update-notification.tsx were still referencing the
old multica-ai/multica repo. Updated both to multica-ai/super-multica.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>