Commit Graph

13 Commits

Author SHA1 Message Date
Bohan Jiang
26924dcc98 fix(desktop): restore Multica icon + WM_CLASS on Linux (MUL-2145) (#2525)
Closes the regression reported in https://github.com/multica-ai/multica/issues/2515 that
PR #2437 only half-fixed in v0.2.31.

Two gaps remained on Ubuntu/GNOME:

1. The .deb shipped only the source 1024×1024 PNG under
   /usr/share/icons/hicolor/, with no usable smaller sizes. GNOME's hicolor
   lookup walks 16…512 and falls back to the theme default when none
   match, so the launcher had no icon. The auto-generation pass in
   electron-builder silently produced only the source size for us. Drop
   pre-rendered 16/24/32/48/64/128/256/512 PNGs into build/icons/ and
   point `linux.icon` at the directory so packaging stops depending on
   the toolchain re-running that generation correctly.

2. WM_CLASS at runtime was `@multica/desktop`, while the .desktop file
   declared `StartupWMClass=Multica`. PR #2437 assumed Electron derives
   WM_CLASS from electron-builder.yml's `productName`, but Electron
   reads `app.getName()`, which reads the *packaged ASAR's* package.json
   — productName if present, otherwise name. Our source
   apps/desktop/package.json had no top-level productName, so the ASAR
   carried only `name: "@multica/desktop"` and Chromium emitted that as
   WM_CLASS, breaking the .desktop association and the dock icon.

   Fixed in two anchors for belt-and-braces: add
   `"productName": "Multica"` to apps/desktop/package.json (so the ASAR
   carries it and app.getName() resolves correctly by default), and call
   `app.setName("Multica")` in the production branch alongside the
   existing dev-only setName so a future regression in package.json or
   the build pipeline cannot silently re-break WM_CLASS.

The `StartupWMClass: Multica` declaration in electron-builder.yml stays
pinned and the surrounding comment has been rewritten to record the
correct WM_CLASS derivation.

Verification on a real Ubuntu install:
- `dpkg-deb -c multica-desktop-*-linux-amd64.deb | grep hicolor` lists
  ≥8 sizes.
- `xprop WM_CLASS` on the running window prints `"multica", "Multica"`.
- Launcher and dock both show the Multica logo with no manual
  ~/.local/share/icons workaround.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-13 17:31:52 +08:00
Bohan Jiang
00ba0aa4e6 fix(desktop): replace Electron placeholder icons with Multica asterisk for Windows + Linux (#2248)
Both `apps/desktop/build/icon.ico` (Windows installer + Multica.exe) and
`apps/desktop/build/icon.png` (Linux deb/rpm/AppImage) were the default
electron-vite scaffold "atom" placeholder. They were never updated when
the macOS `icon.icns` was switched to the Multica asterisk in #1074, and
have shipped as-is in every v0.2.x release including v0.2.26 — closes
GitHub #2195.

Source: 1024×1024 PNG extracted from the existing build/icon.icns
(icon_512x512@2x), so all three platforms now share the same artwork.

- icon.ico: BMP frames at 16/24/32/48/64/128 + PNG-compressed 256×256.
  Matches electron-builder's "≥256×256" requirement and the BMP-then-PNG
  format mix Windows Explorer / NSIS render best across Win10/11.
- icon.png: 1024×1024 RGBA, replacing the previous 512×512 placeholder.

No electron-builder.yml change needed — buildResources: build picks
both files up automatically.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-08 11:42:14 +08:00
Naiyuan Qing
fe358feff0 Reapply "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139) (#1141)
This reverts commit b30fd98605.
2026-04-16 13:16:35 +08:00
Naiyuan Qing
b30fd98605 Revert "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139)
This reverts commit 75d12c26c5.
2026-04-16 12:26:40 +08:00
Naiyuan Qing
75d12c26c5 feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)
* Reapply "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)

This reverts commit 9b94914bc8.

* compat: legacy URL redirect + localStorage double-write for safe rollback

The first attempt at this refactor (#1131) was reverted because existing
users on old URLs (/issues, /projects, etc.) hit 404 immediately after
deploy, and rolling back left them with empty dashboards — the legacy
code reads localStorage["multica_workspace_id"] to attach a workspace
to API requests, but the new code had stopped writing that key.

Two compat layers added on top of the restored refactor:

1. proxy.ts now intercepts legacy route prefixes (/issues/*, /projects/*,
   /agents/*, /inbox/*, /my-issues/*, /autopilots/*, /runtimes/*,
   /skills/*, /settings/*). Logged-in users with a last_workspace_slug
   cookie are 302'd to /{slug}/{rest}, preserving their deep link. Users
   without the cookie bounce through / where the landing page picks a
   workspace client-side. Unauthenticated users go to /login.

2. Both layouts now double-write the workspace id to the legacy
   localStorage key on every workspace entry. New code ignores this key
   — it exists solely so that if this PR ever gets reverted again, the
   legacy build reading the key would still find the correct workspace
   and avoid the empty-dashboard symptom users saw during the rollback.

Net effect: any direction of deploy ↔ rollback is now cache-compatible,
and any direction of old bookmark → new route resolves without 404.

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

* fix(platform): defer rehydrateAllWorkspaceStores to a microtask

Same React 19 render-phase restriction that forced setCurrentWorkspace
to defer its subscriber notifications. rehydrateAllWorkspaceStores
synchronously calls each persist store's rehydrate, which setState()s
the store, which schedules updates on any subscribed component. When
the workspace layout's render-phase ref guard invoked this, React
complained that SearchCommand (a store subscriber) couldn't be
re-rendered while WorkspaceLayout was still rendering.

Fix: queueMicrotask the rehydrate loop and add a pending-flag guard so
rapid workspace switches coalesce into one rehydrate on the final slug.
Persist stores tolerate one microtask of staleness — they hold UI
preferences, not correctness-critical state.

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-16 12:23:41 +08:00
Naiyuan Qing
9b94914bc8 Revert "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)
This reverts commit 59ace95a1e.
2026-04-16 11:56:15 +08:00
Naiyuan Qing
59ace95a1e feat: workspace URL refactor + slug-first API identity (#1131)
* feat: workspace URL refactor + slug-first API identity

Make the URL the single source of truth for workspace identity.
All workspace-scoped URLs now carry the workspace slug as the first
path segment (/{slug}/issues, /{slug}/projects, etc.), matching the
industry standard (Linear, Notion, Vercel, GitHub).

## Key architectural changes

**URL-driven workspace identity:**
- Web routes moved under app/[workspaceSlug]/(dashboard)/
- Desktop routes nested under /:workspaceSlug
- paths.ts builder centralises all URL construction
- reserved-slugs validation (backend + frontend + DB migration audit)

**Slug-first API contract:**
- Frontend sends X-Workspace-Slug header (from URL) instead of X-Workspace-ID (UUID)
- Backend middleware resolves slug → UUID via GetWorkspaceBySlug, falls back to
  X-Workspace-ID for CLI/daemon backwards compatibility
- WebSocket auth accepts ?workspace_slug query param with SlugResolver callback

**State cleanup:**
- Deleted: useWorkspaceStore (Zustand mirror), switchWorkspace/hydrateWorkspace/
  clearWorkspace, localStorage["multica_workspace_id"], api._workspaceId
- useCurrentWorkspace() derives from URL slug + React Query workspace list
- useWorkspaceId() is now a bridge hook (no Context, derives from useCurrentWorkspace)
- WorkspaceIdProvider removed from DashboardGuard
- Paired module vars (slug + UUID) in workspace-storage.ts for non-React consumers

**Layout simplified:**
- Render-phase ref guard sets workspace context synchronously (no async gate)
- DashboardGuard handles auth redirect, loading state, and workspace resolution
- Subscriber notifications deferred via queueMicrotask (React 19 compat)
- persist namespace uses slug (immutable) instead of UUID

## Issues resolved

MUL-43 (share links), MUL-509 (mobile workspace switch), MUL-723 (workspace in URL),
MUL-727 (create workspace flash), MUL-728 (delete workspace no-navigate),
MUL-820 (sidebar Join not switching)

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

* fix: resolve code review C3/C4/C5/C6 — desktop deadlock + hardcoded paths

C3: Desktop OnboardingGate was calling useCurrentWorkspace() outside
WorkspaceSlugProvider → always null → permanent onboarding deadlock.
Rewrite to use useQuery(workspaceListOptions()) which reads React Query
cache directly without slug context. Remove DashboardGuard from
DesktopShell (auth gating handled by AppContent, workspace routing by
WorkspaceRouteLayout per-tab).

C4: Landing page "Dashboard" links hardcoded /issues (no longer valid).
Changed to / — proxy handles redirect to /{lastSlug}/issues.

C5: autopilots-page.tsx had one hardcoded /autopilots/${id} link.
Changed to wsPaths.autopilotDetail(id).

C6: inbox-page.tsx hardcoded /inbox paths. Changed to wsPaths.inbox().

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

* fix(desktop): wrap shell in WorkspaceSlugProvider from module var

AppSidebar calls useWorkspacePaths() → useRequiredWorkspaceSlug() which
throws outside WorkspaceSlugProvider. In the desktop shell, the sidebar
renders at the shell level (outside any tab's WorkspaceRouteLayout).

Fix: DesktopShell reads the current slug via useSyncExternalStore on
the workspace-storage singleton. When slug is available, wraps the
entire shell in WorkspaceSlugProvider. When null (first mount before
any tab's WorkspaceRouteLayout sets it), shows a loading spinner.

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

* fix(desktop): migrate old tab paths + fix shell slug deadlock

Tab store rehydration: old-format paths like "/issues/abc" (missing
workspace slug prefix) are reset to "/" so IndexRedirect picks the
correct workspace. Detection: if the first segment is a known route
name (issues, projects, etc.) rather than a workspace slug, it's an
old-format path.

Desktop shell: TabContent must always render (not gated behind slug
check) so WorkspaceRouteLayout can mount and call setCurrentWorkspace.
Only sidebar and shell-level UI (chat, modals, search) gate on slug
being present.

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-16 11:53:09 +08:00
LinYushen
0e8a7b1734 fix(desktop): make packaged app usable for fresh accounts (#1074)
* feat(desktop): add macOS app icon

Replace the default electron-vite scaffold icon with the Multica asterisk
icon. Adds build/icon.icns so electron-builder picks it up automatically
via the `buildResources: build` config — no YAML change needed.

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

* fix(desktop): run electron-vite build inside package script

The package wrapper only ran bundle-cli.mjs and electron-builder, so
electron-builder silently packaged whatever was already in out/. On a
fresh checkout (or after a partial build) this shipped an app with a
missing renderer bundle, which white-screens on launch.

Add an explicit `electron-vite build` step between bundle-cli and
electron-builder so `pnpm package` is self-contained.

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

* fix(desktop): restore shell PATH in main process for GUI launches

macOS/Linux GUI launches inherit a minimal PATH from launchd that omits
~/.zshrc, Homebrew, nvm, ~/.local/bin, and other shell config. Child
processes spawned from the main process — including the bundled multica
CLI used by daemon-manager — inherit the same stripped PATH, so the CLI
fails to locate agent binaries like claude, codex, opencode, etc. with
"no agent CLI found: … ensure it is on PATH".

Use `fix-path` to recover the real shell PATH at startup, then prepend
common install locations (/opt/homebrew/bin, /usr/local/bin,
~/.local/bin) as a fallback for broken shell rc or non-interactive
$SHELL. Runs before setupDaemonManager so every subsequent spawn sees
the corrected PATH.

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

* fix(desktop): show onboarding wizard when authed user has no workspace

Desktop is a single-shell architecture — every route, including
/onboarding, lives inside DashboardGuard. The guard returns its loading
fallback whenever workspace is null, so a fresh account that logs in
with no workspaces ends up stuck on the spinner forever: the
`replace(onboardingPath)` redirect navigates the tab router, but
DashboardGuard still blocks its children because workspace is still
null.

Handle the empty-workspace case in DesktopShell itself: render
OnboardingWizard as a full-screen takeover, bypassing DashboardGuard.
A ref-based flag freezes the "needs onboarding" decision at first
mount so creating a workspace mid-wizard (step 0) doesn't unmount the
wizard and dump the user into the main shell before steps 1-3
(runtime, agent, get started) finish.

Also add a local `bootstrapping` flag in AppContent so DesktopShell
doesn't mount until the deep-link login chain (loginWithToken →
syncToken → listWorkspaces → hydrateWorkspace) fully resolves. Without
it, the shell would briefly see `!workspace` before hydration lands,
causing users with existing workspaces to flash the wizard (or, with
the ref freeze, get stuck in it permanently).

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

* refactor(desktop): extract OnboardingGate with test coverage

Pull the "render onboarding wizard when authed user has no workspace"
logic out of DesktopShell into a dedicated OnboardingGate component.
Replaces the ref-based freeze with a lazy useState initializer
(`useState(() => !hasWorkspace)`), which is React's idiomatic pattern
for "capture a value once at mount". The freeze semantics are unchanged:
creating a workspace in step 0 of the wizard must not unmount it,
because steps 1-3 still need to run; only `onComplete` flips the gate
back to the main shell.

Also de-duplicates the wrapping DesktopNavigationProvider — both branches
of the shell now share a single provider instead of re-mounting one per
branch.

Wire up jsdom + @testing-library/react in the desktop vitest config
(mirroring packages/views) and add three deterministic tests covering:
  1. children render when hasWorkspace is true at mount
  2. wizard stays mounted when hasWorkspace flips to true mid-flow
  3. onComplete transitions the gate to children

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

* refactor(desktop): drop redundant syncToken call in deep-link login

daemonAPI.syncToken was called twice on a deep-link login: once inside
the deep-link handler's bootstrapping chain, and again in the
useEffect([user]) that reacts to the user state change. Both calls spawn
a multica CLI subprocess over IPC, wasting ~1-2s of startup time on the
critical login path.

Keep the [user] effect (it covers the session-restore path too) and
drop the explicit call from the deep-link handler. Net effect: login
latency shrinks, behavior is unchanged.

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-15 17:27:43 +08:00
LinYushen
6bbe059055 feat(desktop): sync package version with CLI via git tag at build time (#1050)
* fix(desktop): ship entitlements.mac.plist so electron-builder can codesign

electron-builder.yml already references build/entitlements.mac.plist
via entitlementsInherit, but the file was missing from the tree, so
`pnpm package` failed at the codesign step with:

  build/entitlements.mac.plist: cannot read entitlement data

Ship the file. It grants the hardened-runtime capabilities the app
actually needs: JIT + unsigned executable memory for V8, disabled
library validation so the Electron process can spawn the bundled
`multica` Go binary as a child process, and network client/server for
the daemon's API and /health endpoints.

Also tweak the root .gitignore: the top-level `build` rule was
shadowing apps/desktop/build/, hiding this config file from git.
Add a scoped exception so apps/desktop/build/ (which holds
electron-builder source resources, not output) is tracked.

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

* feat(desktop): derive package version from git tag at build time

The Desktop app version was hardcoded to "0.1.0" in package.json and
never bumped, while the bundled CLI reports whatever `git describe`
gives at build time. Result: packaging on main produced
desktop-0.1.0.dmg containing multica v0.1.35-14-gf1415e96 — completely
disconnected. Users see two unrelated version numbers for the same
release.

Sync them by using the same source GoReleaser uses for the CLI: the
nearest git tag. A new scripts/package.mjs wrapper runs bundle-cli.mjs,
derives the version via `git describe --tags --always --dirty` (strips
the `v` prefix, falls back to `0.0.0-<hash>` when no tags are
reachable), and invokes electron-builder with
`-c.extraMetadata.version=<derived>` — which overrides package.json at
build time without mutating the tracked file.

On a clean tag commit → "0.1.36"; between tags → "0.1.35-14-gf1415e96"
(valid semver prerelease); dirty tree → same with "-dirty" suffix.

The `package` script in package.json now points to the wrapper.
Passthrough args (--mac, --arm64, etc.) after `pnpm package --` are
forwarded to electron-builder unchanged. Dev and build scripts are
untouched — they continue to use bundle-cli.mjs directly.

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

* feat(desktop): enable macOS notarization and clean artifact names

Two electron-builder.yml tweaks that unblock a proper release:

- `mac.notarize: false` → `true`. Notarization runs in-build via
  notarytool, reading APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID
  from env. electron-builder then staples the ticket before zipping, so
  `latest-mac.yml`'s SHA512s match the published artifacts (critical
  for electron-updater — post-hoc re-stapling would invalidate them).
  Non-mac/CI contributors are unaffected: `pnpm package` already
  requires the Developer ID signing cert, and notarization is a strict
  superset of signing.

- `mac.artifactName` and `dmg.artifactName` now hardcode
  `multica-desktop-${version}-${arch}.${ext}` instead of using
  `${name}`, which expands to `@multica/desktop` for scoped package
  names and literally produced files at `dist/@multica/desktop-*.dmg`.
  The nested `@multica/` path is useless and makes the GitHub Release
  asset URL ugly. New layout is flat: `dist/multica-desktop-<ver>-arm64.dmg`.

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

* fix(desktop): keep local package builds working after notarize: true

Three polish items from review of this PR.

- Local dev regression: `mac.notarize: true` in electron-builder.yml
  made `pnpm package` hard-fail on macs without APPLE_* env vars, even
  for non-publishing local smoke tests. Detect the missing env in
  scripts/package.mjs and pass `-c.mac.notarize=false` for that run
  only. Real release builds (which source apps/desktop/macOS/.env via
  the release-desktop skill) are unaffected. Also logs a clear warning
  so the developer knows notarization was skipped.
- spawnSync previously used `shell: true`, which reassembled argv into
  a shell command string. Zero real-world injection risk given our
  controlled inputs, but dropping it closes the vector at no cost —
  pnpm already puts node_modules/.bin on PATH for script runs so the
  binary is found without a shell wrapper.
- On spawn failure (e.g. electron-builder not found), result.error was
  silently swallowed and the exit was just `1`. Log the underlying
  reason before exiting.

Also refactor so normalizeGitVersion is exportable and guard the main
entry behind an import.meta.url check, enabling unit coverage. New
package.test.mjs covers the six branches: null/empty input, clean tag,
between-tags prerelease, dirty suffix, v-prefixed prerelease tags
(vX.Y.Z-alpha and vX.Y.Z-rc.2), and the 0.0.0-<hash> fallback for
hash-only describe output. vitest.config.ts picks up scripts/**/*.test.mjs.

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

* feat(desktop): commit .env.production for release builds

Bake production backend + app URLs into release packages so `pnpm
package` produces a build that points at multica.ai out of the box.
electron-vite (Vite) reads .env.production automatically in production
mode — no script changes needed.

Values:

  VITE_API_URL   = https://api.multica.ai
  VITE_WS_URL    = wss://api.multica.ai/ws
  VITE_APP_URL   = https://multica.ai

Also parameterize the two hardcoded `https://www.multica.ai` strings
in platform/navigation.tsx's `getShareableUrl` on VITE_APP_URL. The
previous hardcoded host pointed to `www.multica.ai`, which disagrees
with the canonical `multica.ai` we're standardizing on. Shareable
links from the desktop ("Copy link to issue") now match.

The env file is public config, not a secret, so add a scoped exception
to the root .gitignore's `.env*` rule.

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-15 14:12:53 +08:00
Jiayuan Zhang
d4f5c5b16f feat: pivot to AI-native task management platform (#232)
Replace the agent framework codebase with a new monorepo structure
for an AI-native Linear-like product where agents are first-class citizens.

New architecture:
- server/ — Go backend (Chi + gorilla/websocket + sqlc)
  - API server with REST routes for issues, agents, inbox, workspaces
  - WebSocket hub for real-time updates
  - Local daemon entry point for agent runtime connection
  - PostgreSQL migration with 13 tables (issue, agent, inbox, etc.)
  - WebSocket protocol types for server<->daemon communication
- apps/web/ — Next.js 16 frontend
  - Dashboard layout with sidebar navigation
  - Route skeleton: inbox, issues, agents, board, settings
- packages/ui/ — Preserved shadcn/ui design system (26+ components)
- packages/types/ — Full API contract types (Issue, Agent, Workspace, Inbox, Events)
- packages/sdk/ — REST ApiClient + WebSocket WSClient
- packages/store/ — Zustand stores (issue, agent, inbox, auth)
- packages/hooks/ — React hooks (useIssues, useAgents, useInbox, useRealtime)
- packages/utils/ — Shared utilities

Removed: apps/cli, apps/desktop, apps/mobile, apps/gateway,
packages/core, skills/, and all agent-framework code.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 17:55:49 +08:00
Jiayuan Zhang
c50bdd0f84 fix(desktop): reduce tray icon size to match menu bar conventions
Shrink asterisk glyph from 22x22 to 14x14 within the 22x22 canvas
(28x28 within 44x44 @2x), adding proper padding to align with
other macOS menu bar icons.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:18:27 +08:00
Jiayuan Zhang
5e03b4d76b fix(desktop): use asterisk tray icon and show-only menu
- Replace resized app icon with dedicated tray icon matching MulticaIcon
- Remove click-to-toggle behavior, add "Show Main Window" menu item
- Include tray icon PNGs in electron-builder files config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 23:16:56 +08:00
Naiyuan Qing
2c9d1939c0 fix(desktop): update branding and fix code signing for development
- Change productName from "YourAppName" to "Multica"
- Change appId from "YourAppID" to "com.multica.app"
- Add app icons (icns, ico, png) for all platforms
- Disable hardenedRuntime to fix ad-hoc signing on ARM Macs
- Add build:desktop script to root package.json
- Simplify desktop .gitignore with !build/ exception
- Remove unused asarUnpack and entitlements config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 09:49:18 +08:00