mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 20:15:37 +02:00
agent/j/ccbad775
884 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dcd050ca69 |
fix(desktop): set electron-builder publishingType to release (#1242)
Our CLI release flow pre-creates a *published* GitHub Release via `gh release create`. electron-builder's default `publishingType: draft` conflicts with `existingType=release` and causes the DMG/ZIP/blockmaps/ latest-mac.yml uploads to be silently skipped, which breaks electron-updater auto-update on installed clients (observed on v0.2.4, had to fall back to `gh release upload` manually). Explicitly setting `publishingType: release` aligns electron-builder with our release flow so desktop artifacts are uploaded to the existing published release automatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
80a24bf627 |
refactor(desktop): tabs are per-workspace, not cross-workspace (#1239)
* refactor(desktop): tabs are per-workspace, not cross-workspace Tabs are now grouped by workspace in the store; the TabBar shows only the active workspace's tabs, and switching workspace swaps the visible group. Before this change tabs were a flat list that spanned workspaces, which produced a confusing experience: working in acme with three tabs, then switching to butter and back, still showed whatever tabs you happened to open while you were in butter alongside your acme work. The bug had the same shape as the pre-workspace-overlay bug we fixed in #1237 — a concept ("workspace") was encoded in data (tab paths) but ignored by the UI that displayed it (TabBar). The fix is structural: make the data model match the concept. Key changes: - **Schema**: `{ activeWorkspaceSlug, byWorkspace: {slug: {tabs, activeTabId}} }`. The invariant "every tab belongs to a workspace group" is enforced at sanitize time and at migration time; there is no longer a root `/` sentinel. - **NavigationAdapter** detects cross-workspace pushes and delegates to `switchWorkspace(slug, path)` instead of navigating the active tab's router. All existing call sites in shared code (sidebar dropdown, settings post-delete redirect, invite-accept, cmd+k) keep calling `push(paths.workspace(x).issues())` unchanged. - **TabContent** renders only the active workspace's tabs under Activity. Cross-workspace state preservation is an explicit non-goal — switching workspaces should feel like switching. - **WorkspaceRouteLayout** auto-heal no longer navigates the tab router to `/`. Stale-slug cleanup is a store-level op (`validateWorkspaceSlugs`) that drops the whole stale group in one go. - **App.tsx** bootstrap seeds `activeWorkspaceSlug` when null and the user has workspaces; the new-workspace overlay opens/closes based on workspace count independently of any route. - **Persistence migration** (v1 → v2) groups old flat tabs by extracted slug, drops root / transition / reserved-slug tabs, and picks an active workspace from the old active tab's owning group. No data loss for existing users with workspace-scoped tabs. Web is unchanged — tabs are a desktop-only concept. `packages/views`, `packages/core`, `apps/web` are all untouched. `setCurrentWorkspace` in core remains the single source of truth for the API client's workspace header, driven by `WorkspaceRouteLayout` as before. Tests: 19 tab-store tests (sanitize, migration, switchWorkspace, validate, close-last-reseeds, reset). 38 desktop tests total pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: stable selectors + defensive guards on tab-store Addresses self-review findings on #1239. **C1 — perf cliff from unstable selector returns.** The previous `useActiveTab()` selector used `.find()` inside, so every router tick on the active tab (which replaces the Tab object via immutable spread in updateTab / updateTabHistory) forced every subscriber to re-render. Replaced with finer-grained selectors: - `useActiveTabIdentity()` — { slug, tabId } primitives (stable across unrelated updates). - `useActiveTabRouter()` — stable object reference for a tab's lifetime. - `useActiveTabHistory()` — { historyIndex, historyLength } numbers. `useTabHistory` and `DesktopNavigationProvider` now consume the primitive selectors, so back/forward buttons don't churn on every path change. A non-hook `getActiveTab(state)` helper covers the event-handler case. **I1 — `switchWorkspace` no-ops on empty slug.** Defensive guard in case a malformed path ever reaches the adapter's detector. **I2 — merge warns on path/slug mismatch.** Previously silent drop; now `console.warn` makes the condition visible during debugging. **Misc — TabRouterInner takes `tab` prop directly.** Passing the Tab object eliminates a redundant store read per rendered tab. Known follow-up (not this PR): `packages/core/realtime/use-realtime-sync.ts` still uses `window.location.assign` for workspace-deleted eviction — that's a full renderer reload on desktop, which post-refactor wastes the careful in-memory tab state we just set up. Fixing cleanly requires a navigation-callback injection pattern through CoreProvider, which is cross-cutting and deserves its own PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(workspace): navigate away BEFORE leave/delete mutation to avoid CancelledError Symptom: deleting the current workspace logged "current workspace deleted, switching" from the realtime handler and surfaced an "Uncaught (in promise) CancelledError" from TanStack Query's refetchQueries batch. Root cause: a three-way race between the mutation's own invalidateQueries(workspaceKeys.list()), the settings page's navigateAwayFromCurrentWorkspace() fetchQuery, and the realtime workspace:deleted handler's relocateAfterWorkspaceLoss fetchQuery. All three refetched the same query concurrently; TanStack Query cancelled the in-flight loser(s), and the rejection bubbled out of invalidateQueries as an unhandled promise rejection. Fix: invert the order. Compute the destination from the current cached workspace list, navigate immediately, *then* fire the mutation. By the time the backend fires workspace:deleted, the active workspace is already something else — the realtime handler's "current === deleted" check fails and its relocate branch no-ops. Only one refetch happens (the mutation's onSettled), no race, no cancellation. navigateAwayFromCurrentWorkspace no longer needs async/fetchQuery since it reads from cache and returns before the mutation fires. Applies to both Leave and Delete flows. Both web and desktop benefit since the code is in packages/views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(desktop): clear workspace singleton + flex drag strip + defer seeding Three issues that the last round of delete-workspace fixes missed. **1. `setCurrentWorkspace` singleton leaks after delete.** Navigating before the mutation (prior fix) changed the URL but nothing cleared the core platform's currentSlug/currentWsId singleton. Three downstream consumers still believed the deleted workspace was active: - `useRealtimeSync`'s `workspace:deleted` handler: its `getCurrentWsId() === deleted` check fired, triggering a parallel relocate that raced the mutation's invalidate and the settings page's navigate — CancelledError + `window.location.assign` (white screen reload). - Chrome gating: `{slug && <AppSidebar />}` stayed truthy, the sidebar mounted, and `useWorkspaceId` inside it threw because the workspace was gone from the list cache. - API client's `X-Workspace-Slug` header: stale on the next call. Fix: `navigateAwayFromCurrentWorkspace` now calls `setCurrentWorkspace(null, null)` before pushing. The next workspace's `WorkspaceRouteLayout` re-sets the singleton when it mounts; for the last-workspace case, null is the correct state (overlay has no workspace context). Same family as the previous logout bug: persist only writes to storage, reset on logout must also wipe in-memory state. Here the singleton is another in-memory bit that survives a URL change if we don't explicitly clear it. **2. "Cannot update a component while rendering" warning.** The per-workspace-tabs refactor kept the validate+seed call in render phase (matching the pre-refactor pattern). It worked before because `validateWorkspaceSlugs` is idempotent; the new `switchWorkspace` seed is not, and triggers a TabBar re-render during AppContent's render. Moved to `useLayoutEffect` — synchronously after render, before paint, no flicker. **3. Welcome-screen drag region didn't work on desktop.** The absolute-positioned `h-10 z-10` drag strip relied on z-index stacking to beat the content wrapper's no-drag for hit-testing, which wasn't reliable for `-webkit-app-region` on the overlay. Replaced with a flex child (`h-12 shrink-0` at top of the overlay's flex-col), so the drag region owns its own layout space — any pixel in the top 48 is unambiguously drag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(CLAUDE): desktop-specific rules — routing, singleton, drag, UX split Codifies the lessons from the recent desktop refactor series (#1237, #1238, #1239) so future work doesn't re-derive them from bugs. Covers: - **Route categories** (session / transition / error) — explains why `/workspaces/new` and `/invite/:id` are overlay state, not routes, on desktop; stale slugs auto-heal instead of rendering error pages. - **`setCurrentWorkspace` singleton hygiene** — unmount doesn't clear it; any code leaving workspace context must call `setCurrentWorkspace(null, null)` explicitly. - **Workspace destructive operations ordering** — navigate first, mutate after, to avoid the three-way refetch race that surfaces as CancelledError + full-page reload. - **Tab isolation** — tabs are grouped per workspace; cross-workspace push is intercepted by the navigation adapter and translated into switchWorkspace. - **Drag region pattern** — flex child at top, not absolute overlay; `-webkit-app-region` hit-testing is unreliable with z-index stacking. - **UX vs platform chrome split** — UX affordances (Back, Log out, welcome copy) in packages/views/; platform chrome (drag, immersive mode, tab system) in desktop-only code. Also patches the Cross-Platform Development Rules' rule #2 which previously said "add a route in both apps" unconditionally — added the exception for pre-workspace transition flows pointing at the new Desktop-specific Rules section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c2f7dc49f8 |
refactor(desktop): model pre-workspace flows as window overlays, not tab routes (#1237)
Previously /workspaces/new and /invite/:id were tab routes on desktop. That meant the TabBar rendered on top of flows that conceptually aren't "places" the user sits at — creating a workspace or accepting an invite is a one-shot transition, not a session. The mismatch also produced several downstream bugs: tab state persisted these paths, the invite deep link had no clean dispatch target, and NoAccessPage leaked TabBar chrome when a workspace slug went stale. Fix by recognising the underlying category mistake: on desktop, these flows are application state, not routes. Move them to a window-level overlay driven by a small Zustand store; the navigation adapter intercepts pushes to the corresponding paths and routes them to the overlay instead. Web keeps the routes (users need shareable URLs and back-button semantics), so shared view components are reused as-is. UX affordances (Back button when dismissable, Log out escape) live in the shared NewWorkspacePage/InvitePage so both platforms render identical content; the desktop overlay is now a thin platform shell (drag strip + useImmersiveMode) that wraps the shared UX. Web wires onBack based on whether the user has any workspaces. Also addresses several related issues uncovered along the way: - Logout now resets the in-memory tab + overlay stores (previously only localStorage was cleared, so the next login inherited the prior user's tabs). - WorkspaceRouteLayout auto-heals a stale workspace slug by navigating to "/" instead of rendering NoAccessPage — on desktop without a URL bar, "no access" is always stale state, not a legitimate destination. - IndexRedirect overlay lifecycle is bidirectional: opens when wsList is empty, closes when it becomes non-empty (realtime workspace:added would otherwise leave the overlay stuck open). - tryRouteToOverlay resets the current tab to "/" when opening the new-workspace overlay; otherwise workspace-scoped components under the overlay continue to render and throw when the workspace they reference disappears from the cache (reproducible by deleting the last workspace from Settings). - handleDeepLink now accepts multica://invite/<id>, IPC'd through to the renderer and opened as an invite overlay. Email template still links to https:// (unchanged), but the desktop dispatch path is now wired for a future "open in desktop app" bridge. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d7a8e9041e |
refactor(landing): tighten hero — CTAs, install copy, works-with wrap, LCP priority (#1227)
* refactor(landing): tighten hero — CTAs, install copy, works-with wrap, LCP priority - Drop GitHub button from hero CTAs (already in header) so the primary Start / Download Desktop pair is the clear path. - Split InstallCommand: outer is no longer a <button>, so text selection no longer fights with copy. Mobile gets full-width with break-all; desktop keeps the compact pill. Copy button has aria-label. - Fix invalid `hover:bg-white/8` opacity to `hover:bg-white/[0.08]` so the install pill's hover background actually renders. - Add `flex-wrap` and gap-y to the "Works with" row so the label + 5 logos can stack on small screens instead of overflowing horizontally. - Move `priority` from the decorative backdrop image onto the product hero image (the actual LCP candidate) to stop background bytes from starving the foreground. * refactor(landing): remove install command from hero Per design feedback, the install command pill is removed from the hero. The download path now flows through the Download Desktop CTA only; install instructions remain available in the docs and README. |
||
|
|
6e980925cf |
chore(desktop): DESKTOP_APP_SUFFIX env for parallel-worktree dev (#1215)
Dev Electron uses a single userData path ("Multica Canary") derived from
the app name, which also locates the single-instance lock. Two worktrees
running dev simultaneously fight for that lock — the second `app.quit()`s
silently before opening a window.
DESKTOP_APP_SUFFIX appends to the app name + userData path so each
worktree can claim its own lock:
DESKTOP_APP_SUFFIX=foo → "Multica Canary foo"
Default (no env var) keeps behavior unchanged.
Complements the existing DESKTOP_RENDERER_PORT env from #1210 so a full
"run a second dev Electron" setup looks like:
DESKTOP_RENDERER_PORT=15173 DESKTOP_APP_SUFFIX=foo pnpm dev:desktop
|
||
|
|
8816e1669c |
feat(desktop): brand dev build as Multica Canary with bundled icon (#1210)
* feat(desktop): brand dev build as Multica Canary with bundled icon pnpm dev:desktop ran under the stock Electron name and default icon, making it indistinguishable from any other Electron dev app in the dock. Set a Canary app name + userData path and point the macOS dock icon and BrowserWindow icon at the bundled resources/icon.png so the dev build is visually branded. * feat(desktop): allow overriding renderer port via DESKTOP_RENDERER_PORT Lets a second worktree run `pnpm dev:desktop` while a primary checkout already holds the default Vite dev port 5173 — required to actually exercise the "Multica Canary" branding in isolation. * feat(desktop): rebrand Electron.app Info.plist so dev shows Multica Canary app.setName() can't override the macOS menu bar title or Cmd+Tab label — those come from CFBundleName baked into the running bundle's Info.plist. Patch the bundled Electron.app's plist during `pnpm dev:desktop` so dev launches read "Multica Canary" everywhere, not "Electron". Idempotent; unlinks before rewriting so we don't mutate a pnpm-store inode shared with other projects. |
||
|
|
fe6208c61f |
fix(desktop): strip leading '--' so --publish reaches electron-builder (#1199)
When invoked as `pnpm package -- --mac --arm64 --publish always`, the bare `--` separator that pnpm inserts was forwarded into electron-builder's argv. This terminated option parsing, causing `--publish always` to be treated as positional arguments instead of a named flag. As a result electron-builder built locally but never uploaded artifacts to the GitHub Release (isPublish: false). Add `stripLeadingSeparator()` to remove the leading `--` before passing args through. Includes unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
336f90fd26 |
fix(desktop): new tab inherits current workspace + guard against malformed tab paths (#1198)
* fix(desktop): new tab inherits current workspace + guard against malformed tab paths
Three layered fixes for the same root cause: tab URLs were being
constructed without a workspace slug in some code paths, triggering
NoAccessPage whenever the router interpreted the first segment as a
(non-existent) workspace slug.
## Layer 1 — tab-bar "+" button now inherits current workspace
The handler had a hardcoded `path = "/issues"` left over from before
the slug URL refactor. Without a workspace prefix, the router saw
`workspaceSlug = "issues"` and rendered NoAccessPage. Read
`getCurrentSlug()` and build `/{slug}/issues` instead. Falls back to
"/" (→ IndexRedirect) when there is no current workspace.
This matches terminal/IDE new-tab semantics: new tab opens in the
same workspace as the active tab, not in `wsList[0]`.
## Layer 2 — validateWorkspaceSlugs runs synchronously
PR #1178 added startup validation of persisted tab slugs against the
current workspace list, but ran it in a useEffect. useEffect fires
AFTER commit, so the initial render would briefly show NoAccessPage
on a stale slug before the effect reset the tab path. Moving the call
into render phase eliminates that flash; zustand supports setState in
render, and the validator is idempotent (early-returns if nothing
changed) so this doesn't loop.
## Layer 3 — tab store rejects malformed paths at construction
Any path whose first segment is a reserved slug (e.g. "/issues",
"/login") clearly lacks a workspace prefix and is a caller bug.
sanitizeTabPath catches these at makeTab time, rewrites to "/", and
logs a console.warn naming the offending path so the bug can be fixed
at source. Any future new-tab entry point that forgets the slug will
not reach NoAccessPage.
Net effect: NoAccessPage is reserved for its legitimate purpose —
users navigating to URLs they genuinely don't have access to — and
can no longer be triggered by system bugs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* review: read new-tab workspace from active tab + unify sanitize + add tests
Three follow-ups from self-review of PR #1198:
1. Resolve the current workspace from the active tab's path instead of
from getCurrentSlug(). With N tabs mounted under <Activity>, every
WorkspaceRouteLayout calls setCurrentWorkspace() in render — the
singleton ends up holding "whichever tab rendered last", which is
non-deterministic. activeTabId is the unambiguous source of truth
for "which workspace is the user actually looking at right now".
2. Unify the persist merge's stale-path detection with sanitizeTabPath.
The merge previously checked ROUTE_ICONS (dashboard segments only);
sanitizeTabPath uses isReservedSlug (dashboard + auth + platform +
RFC 2142 + hostname confusables). Same code path now, wider
coverage, and one source of truth.
3. Add unit tests for sanitizeTabPath: root pass-through, global paths,
valid workspace-scoped paths, malformed paths (reserved first
segment) rejected with console.warn, and user slugs that happen to
look path-like but aren't reserved.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
6d6bc5a6f2 |
fix(routing): rename /new-workspace to /workspaces/new + extend reserved slug list (#1188)
* fix(routing): rename /new-workspace to /workspaces/new + extend reserved slug list
Two related changes:
1. Rename the global workspace-creation route from /new-workspace to
/workspaces/new. The hyphenated word-group `new-workspace` is a
common user workspace name (last deploy was blocked by a real user
with exactly this slug). Industry consensus from auditing Linear,
Vercel, Notion, Slack, GitHub: zero major SaaS uses hyphenated
word-group root routes — they all use single words or `/{noun}/{verb}`
pairs. Reserving the noun `workspaces` automatically protects the
entire `/workspaces/*` subtree, so future workspace-related routes
(`/workspaces/{id}/edit`, `/workspaces/{id}/billing`, etc.) need no
additional reserved slugs or audit migrations.
2. Extend the reserved slug list to cover the minimal set recommended by
the URL-design audit: full auth flow vocab, RFC 2142 mailbox names
(postmaster, abuse, noreply...), hostname confusables (mail, ftp,
static, cdn...), and likely-future platform routes (docs, support,
status, legal, privacy, terms, security, etc.). Production data
audit confirmed zero conflicts for every newly added slug, so
migration 047 (the safety net) passes cleanly.
Slugs intentionally NOT added despite being in scope of the audit:
admin, multica, new, setup, www. Each has one production workspace
already using it; adding them now would block deploy. They will be
handled in a follow-up PR via owner outreach + targeted rename.
Also adds a CLAUDE.md convention rule: new global routes MUST use a
single word or `/{noun}/{verb}` pair, never hyphenated word groups.
This prevents the pattern from regenerating itself.
This PR does NOT resolve the currently-blocked prd deploy — that requires
the existing `slug='new-workspace'` workspace (owner: Dhruv Raina) to be
renamed by ops. After that workspace is renamed and migration 046 passes,
this PR's migration 047 will also pass on its first run.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* review: drop migration 046, sweep stale comments, drive reserved test from map
Address code review on PR #1188:
1. Delete migration 046 (audit_new_workspace_slug). It audits "new-workspace"
which is no longer a reserved slug after this PR's rename. Removing 046
has an unexpected upside: it directly unblocks the currently-stuck prd
deploy. Migration 046 had never successfully applied (it was the source
of the deploy block); the audit-only nature means down-rollback is a
no-op. The user workspace previously caught by 046 (slug='new-workspace',
owner: Dhruv Raina) is now safe — `new-workspace` is no longer reserved,
so the slug correctly resolves to that workspace and the global route
`/workspaces/new` doesn't shadow it.
2. Refactor workspace_test.go to drive its reserved-slug list from the
reservedSlugs map directly via `for slug := range reservedSlugs`. The
previous hand-copied list was already drifting (40-ish entries vs 58 in
the map). Now drift is impossible.
3. Sweep ~10 stale `/new-workspace` references in code comments to
`/workspaces/new`. Comments only — runtime unchanged. The references
in reserved-slugs.ts/workspace_reserved_slugs.go and CLAUDE.md are
intentionally kept as anti-pattern examples ("don't add hyphenated
word-group root routes like /new-workspace").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
fe13259cc6 |
fix(desktop): validate persisted tab slugs against current workspace list (#1178)
Desktop tabs persist their full path to localStorage (multica_tabs), so a tab path like /naiyuan/issues survives app restarts, account switches, and workspace deletions. Any stale slug caused WorkspaceRouteLayout to render NoAccessPage immediately on login — the user saw "Workspace not available" every time they opened the app, with no way to recover except manually opening a new tab or clearing localStorage. Root cause: persisted URL strings outlive the server-state they reference. The auth initializer fetches a fresh workspace list on every startup, but nothing validated the tab paths against it. Fix: add tab-store.validateWorkspaceSlugs(validSlugs). Runs on every change to the workspace list query data (login, background refetch, realtime workspace:deleted). Any tab whose first path segment isn't in the valid slug set is reset to `/`, where IndexRedirect picks a live workspace (or /new-workspace if the user has none). Idempotent, so over-triggering is safe. Tabs on global paths (/login, /new-workspace, /invite/...) are left alone. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
6a2432b16b |
refactor: remove onboarding flow, fix daemon zero-workspace bootstrap (#1175)
* fix(daemon): allow startup with zero workspaces The daemon used to fail fast with "no runtimes registered" when the initial workspace sync returned zero workspaces. This masked a latent bug: a newly-signed-up user has no workspaces yet, so the daemon would crash immediately after login instead of waiting for the first workspace to be created. workspaceSyncLoop already polls every 30s (daemon.go:107, 365) to discover new workspaces — the fail-fast check at startup was bypassing this dynamic discovery. Remove the check so the daemon stays resident and picks up the first workspace whenever it appears. PR #1001 partially addressed this for the "server has workspaces but local CLI config is empty" case. This finishes the job for the true zero-workspace state, which until now was masked by the onboarding wizard always creating a workspace before the daemon started. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(views): extract CreateWorkspaceForm for reuse Modal and the upcoming /new-workspace page share the same form + mutation + slug validation. Extract to a shared component so they can't drift. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(views): add NoAccessPage for unknown or inaccessible workspace slugs Rendered when the URL slug doesn't resolve to a workspace the user has access to. Deliberately doesn't distinguish 404 vs 403 to avoid letting attackers enumerate workspace slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(paths): add /new-workspace route and reserve slug on both sides Adds paths.newWorkspace() builder, registers /new-workspace as a global (pre-workspace) prefix, and reserves the "new-workspace" slug on both frontend and backend (kept in sync per convention). Existing "onboarding" reservation retained — removing it would desync FE/BE and leaves no future fallback if an onboarding route is revived. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(migrations): audit no existing workspace uses 'new-workspace' slug Migration 046 blocks deploy if any workspace in the DB has slug = 'new-workspace', which would shadow the new global workspace creation route at /new-workspace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add /new-workspace route on web and desktop Renders the CreateWorkspaceForm as a full-page workspace creation flow, used as the destination for first-time users with zero workspaces. Replaces the 4-step onboarding wizard with a single form. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show NoAccessPage on unknown workspace slug, hold null during active removal Layouts render NoAccessPage when the URL slug doesn't resolve to an accessible workspace — except when the slug previously resolved during this layout instance's lifetime. URL and cache are two asynchronous signals: there will always be a short window where the URL still points at the old workspace but the cache has already been invalidated (e.g. just after a delete/leave mutation, or a realtime workspace:deleted event). Rendering NoAccessPage during that window would flash "Workspace not available" with recovery buttons in front of a user who just deleted the workspace themselves — jarring and wrong. useWorkspaceSeen classifies the two cases: - slug was seen before, now gone → user's intent is changing (caller is navigating away); render null, no flash - slug never seen → user is genuinely looking at an inaccessible workspace (stale bookmark, revoked access, link from a former teammate); render NoAccessPage with recovery options NoAccessPage deliberately does not distinguish 404 vs 403 to avoid letting attackers enumerate workspace slugs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: redirect zero-workspace users to /new-workspace instead of /onboarding Switches 8 call sites and the CLI: - Web: login, auth callback, landing redirect-if-authenticated - Desktop: routes.tsx IndexRedirect - Shared: dashboard guard, invite page fallback, workspace-tab on delete, realtime sync on workspace loss - CLI: cmd_login.go waitForOnboarding now opens /new-workspace Also adds /new-workspace to navigation store's lastPath exclusion list so it doesn't get persisted as a 'last visited' page. Adds a desktop App.tsx effect that restarts the daemon when workspace count transitions 0 → ≥1, so first-workspace creation triggers immediate daemon pickup rather than waiting up to 30s for the daemon's workspaceSyncLoop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove onboarding flow The 4-step onboarding wizard (workspace → runtime → agent → demo issues) is replaced by: - /new-workspace: a single-page workspace creation form (Phase 3) - NoAccessPage: explicit feedback when a slug doesn't resolve (Phase 4) - daemon zero-workspace bootstrap (Phase 1) so the daemon doesn't crash before the user creates their first workspace - desktop daemon restart on first workspace creation (Phase 5) for instant pickup instead of the 30s workspaceSyncLoop tick Deletions: - packages/views/onboarding/ (OnboardingWizard + 4 step components + tests) - apps/web/app/(auth)/onboarding/page.tsx - apps/desktop/src/renderer/src/components/onboarding-gate.tsx (+test) - OnboardingGate wrapper in desktop-layout.tsx - OnboardingRoute + /onboarding route in desktop routes.tsx - paths.onboarding() builder + /onboarding from GLOBAL_PREFIXES - packages/views/package.json onboarding export - /onboarding from navigation store's EXCLUDED_PREFIXES Retained (intentional): - 'onboarding' in RESERVED_SLUGS (both FE + BE) — kept for FE/BE sync and future-proofing if /onboarding is ever revived Also drops 4 demo issues that onboarding used to create on the new workspace ('Say hello', 'Set up repo', etc.). New workspaces are now fully empty; all list views already render empty-state UI correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: clean stale 'onboarding' references in comments and CLI helpers Batch cleanup of references to the removed onboarding flow: - 13 comment sites mentioning 'onboarding' updated to reflect the new /new-workspace flow or removed where no longer accurate - CLI waitForOnboarding renamed to waitForWorkspaceCreation (function name + docstring); behavior unchanged The 'onboarding' reserved slug entries (frontend + backend) are intentionally retained — see prior commit rationale. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(views): extract shared NewWorkspacePage shell The web (/new-workspace) and desktop (NewWorkspaceRoute) pages had identical outer layout — same container, heading, and copy — with only the onSuccess navigation primitive differing. That's exactly the No-Duplication Rule pattern: extract the shared UI, inject the platform-specific behavior. The apps now only own the thin auth guard (web needs it, desktop routes below WorkspaceRouteLayout already handle it) and the onSuccess → navigate call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove rollback compat layer and tighten daemon restart trigger Two cleanup items: 1. Drop localStorage['multica_workspace_id'] double-write in both workspace layouts. That write was added as a rollback safety net for the workspace-slug URL refactor (PR #1138) — the refactor has since landed and stabilized, so the compat shim is no longer needed. Per CLAUDE.md: don't keep compat layers beyond their purpose. 2. Tighten the desktop daemon-restart trigger. The previous ref-based logic fired a restart on any 0→1 workspace-count transition, including account switches (user A logout → user B login). Scope it precisely to 'this session started with zero workspaces and just gained one' using a three-state ref (null=undecided, true=empty-start, false=already-restarted-or-started-nonempty). Account switches are already handled by daemon-manager.ts on token change, so this avoids a redundant restart there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(auth): redirect to /login on logout and unauthenticated workspace visits Two gaps previously left users stuck on blank workspace pages: 1. app-sidebar logout() cleared all state but never moved the URL. The current path is /{workspaceSlug}/... which has no meaning without auth; the workspace layout would then see user=null, render null (via the hasBeenSeen short-circuit), and the user saw a blank page thinking logout didn't work. 2. The workspace layouts (web + desktop) had no !user handling at all. Any path that leaves user=null — token expiration, cross-tab logout, or fresh visit to a workspace URL without a session — resulted in the same blank screen. Fix: - app-sidebar.logout() explicitly push(paths.login()) after authLogout() to cover the primary (user-initiated) logout path. - Both workspace layouts get a defensive useEffect that redirects to /login whenever auth has settled and user is null. Covers token expiration, realtime logout, and any other silent session loss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3a5f94cbdd |
docs: add v0.2.1 changelog (2026-04-16) (#1177)
* docs: add v0.2.1 changelog entry (2026-04-16) * docs: swap desktop download with workspace URL refactor in v0.2.1 |
||
|
|
94c9d2807a |
fix(core): collapse workspace rehydrate side effect into setCurrentWorkspace (#1164)
Problem ------- On desktop, creating a new tab triggered thousands of chat-store rehydration logs per second (sustained for seconds). Same session, same workspace — nothing actually changed. `pnpm test` was clean; the bug only manifests at runtime with React 19 Activity + multi-tab. Root cause ---------- Every tab's WorkspaceRouteLayout kept its own `syncedSlugRef` to decide "did slug change since last sync". That model assumes one layout instance equals one workspace context — true on web, false on desktop where N tabs each mount their own layout. Activity remounts + tab-router-sync stirring the tab store caused per-layout refs to drift out of agreement with the module-level truth, so each ref independently called `rehydrateAllWorkspaceStores()`. The existing microtask dedup only coalesced same-tick calls; successive ticks each scheduled another iteration through every registered rehydrate fn. Fix --- Move the "did slug actually change?" decision to where the truth lives: inside `setCurrentWorkspace` itself. The singleton now: - Returns immediately when the slug is already current (idempotent). - Fires slug subscribers + persist rehydrate as internal side effects when (and only when) the slug transitions. Layouts are simplified to "feed the URL slug in"; they no longer maintain a ref guard or call rehydrate explicitly. N tabs feeding the same slug is naturally a no-op after the first — the model no longer depends on "one layout instance" as an implicit invariant. Also hardens the original render-time race that motivated the v2 refactor: both layouts now gate on `!listFetched || !workspace` so `useWorkspaceId()` in descendants is guaranteed non-null. Public API ---------- `rehydrateAllWorkspaceStores` removed from `@multica/core/platform` exports — it's now purely an internal effect of `setCurrentWorkspace`. The function itself is deleted; the rehydrate loop lives inline in `setCurrentWorkspace`. Tests ----- Four new tests covering the new semantics: single rehydrate on mount, same-slug noop across repeat calls, real workspace switch fires again, logout → re-entry into same workspace fires again. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
fa804c2215 |
feat(web): add Desktop download entry to landing page (#1093)
Add a "Download Desktop" button in the hero section alongside the existing CTA and GitHub buttons, linking to the latest GitHub release. Also add a Desktop link in the footer product group for both EN and ZH. |
||
|
|
e3a1b951fb |
fix(desktop): allow dev and production instances to coexist (#1155)
Dev mode now uses a separate app name ('Multica Dev') and userData path
before acquiring the single-instance lock, so the lock file no longer
collides with the packaged production app. The AppUserModelId is also
differentiated (ai.multica.desktop.dev vs ai.multica.desktop).
This follows the same pattern VS Code uses for Stable / Insiders
coexistence: isolate identity before requestSingleInstanceLock().
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
8c518c350a |
feat(agent): add Pi agent runtime support (#1064)
* feat(agent): add Pi agent runtime support
Add Pi as a new agent runtime provider, following the established adapter
pattern. Pi CLI outputs JSONL events which are parsed for messages, tool
calls, and usage tracking.
Backend:
- New piBackend implementing the Backend interface (pi.go)
- Pi CLI discovery via MULTICA_PI_PATH env var or PATH lookup
- JSONL event stream parsing (agent_start, message_update, thinking_update,
tool_execution_start/end, agent_end)
- Usage scanner for ~/.pi/sessions/*.jsonl files
- Runtime config injection via AGENTS.md
- Skill injection to .pi/agent/skills/
Frontend:
- Pi provider logo (teal π icon)
- Pi label in transcript dialog
Docs:
- Updated all provider lists in README, CLI_INSTALL, and docs
* fix(agent): filter Pi usage scanner to agent_end events only
Address review feedback: restrict usage parsing to agent_end events
which contain cumulative totals, preventing potential inaccuracy if
Pi adds usage fields to other event types in the future.
* fix(agent): align Pi runtime with real CLI flags, event schema, and custom_args
- Flags: Pi's CLI uses `--mode json` (not `--output-format jsonl`), has no
`--yolo` (explicit `--tools` allowlist instead), takes the prompt as a
positional argument (not `-p <prompt>`), splits model as
`--provider <name> --model <id>`, and treats `--session` as a file path
that must exist before spawn.
- Event parsing: rewrite the stream event struct to match Pi's actual
JSON event schema (`message_update.assistantMessageEvent.delta`,
`turn_end.message.usage.{input,output,cacheRead,cacheWrite}`, etc.).
- Sessions: generate/persist session files under ~/.multica/pi-sessions/
and use the file path as the opaque SessionID returned to the daemon.
- Usage scanner: read assistant `message` events from the same session
files (Pi's session-file schema, distinct from the stdout stream).
- Custom args: consume `ExecOptions.CustomArgs` via `filterCustomArgs`
with a Pi-specific blocked set (`-p`, `--print`, `--mode`, `--session`)
so Pi matches the pattern shared by every other agent backend.
|
||
|
|
b5c6a9b8f0 |
fix(desktop): reserve traffic-light space and surface trigger when sidebar is hidden (#1144)
The top bar pads `pl-20` for the macOS traffic lights only when `state === "collapsed"`, but the shadcn sidebar also hides itself in mobile mode (<768px) where `state` stays `"expanded"` and only `isMobile` flips. In that case tabs slid under the traffic lights and no UI affordance existed to bring the sidebar back (since the in-sidebar trigger went off-canvas with it). Treat both as "sidebar not in main flow", apply the padding, and render a `SidebarTrigger` in the header (with `no-drag` so the window drag region doesn't swallow the click). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
fe358feff0 |
Reapply "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139) (#1141)
This reverts commit
|
||
|
|
b30fd98605 |
Revert "feat: workspace URL refactor v2 + rollback-safe compat layer (#1138)" (#1139)
This reverts commit
|
||
|
|
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 |
Revert "feat: workspace URL refactor + slug-first API identity (#1131)" (#1137)
This reverts commit
|
||
|
|
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>
|
||
|
|
5a44c255fe | docs: add v0.2.0 changelog entry (2026-04-15) (#1078) | ||
|
|
8a55473bb8 |
fix(desktop): evaluate daemon spawn env lazily to pick up PATH fix (#1088)
DESKTOP_SPAWN_ENV was a top-level const in daemon-manager.ts that snapshotted process.env at module load. Because ESM imports are hoisted and evaluated before main/index.ts runs fix-path, the snapshot captured launchd's minimal PATH — missing ~/.local/bin, Homebrew, etc. The main process then had the corrected PATH, but every spawned daemon inherited the stale one and failed with "no agent CLI found" on fresh GUI launches. Convert it to desktopSpawnEnv() so process.env is read at call time, after fix-path has already updated it. Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ce94c80f5a |
fix(desktop): read VITE_APP_URL for Google login external redirect (#1086)
The desktop login was reading VITE_WEB_URL, which is defined nowhere in the committed env files. In production builds the variable was undefined, so Google login opened http://localhost:3000/login?platform=desktop instead of https://multica.ai/login?platform=desktop. Switch to VITE_APP_URL, which is already set in apps/desktop/.env.production and is the same variable platform/navigation.tsx uses for shareable links. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
176f1bfdbb |
refactor(desktop): keep only create-workspace step in onboarding (#1083)
Fresh desktop accounts no longer need to walk through runtime, agent, and get-started steps before reaching the app. Once the workspace is created, the onboarding gate hands off directly to the main shell. Web onboarding is unchanged. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
ce447c7f06 |
feat(agent): add custom CLI arguments support (#986)
* feat(agent): add custom CLI arguments support Allow users to configure custom CLI arguments per agent that get appended to the agent subprocess command at launch time. This enables use cases like specifying different models (--model o3), max turns, or other provider-specific flags without needing separate runtimes. Changes: - Add custom_args JSONB column to agent table (migration 041) - Update API handler to accept/return custom_args in create/update - Pass custom_args through claim endpoint to daemon - Append custom_args to CLI commands for all agent backends - Add ExecOptions.CustomArgs field in agent package - Add Custom Args tab in agent detail UI - Add --custom-args flag to CLI agent create/update commands Closes MUL-802 * fix(agent): filter protocol-critical flags from custom_args Add per-backend filtering of custom_args to prevent users from accidentally overriding flags that the daemon hardcodes for its communication protocol (e.g. --output-format, --input-format, --permission-mode for Claude). This follows the same pattern as custom_env's isBlockedEnvKey: we only block the small, stable set of flags that would break the daemon↔agent protocol — not every possible dangerous flag. Workspace members are trusted for everything else. Each backend defines its own blocked set: - Claude: -p, --output-format, --input-format, --permission-mode - Gemini: -p, --yolo, -o - Codex: --listen - OpenCode: --format - OpenClaw: --local, --json, --session-id, --message - Hermes: none (ACP is positional) Includes unit tests for the filtering logic. * fix(agent): address code review nits for custom_args - Replace module-level `nextArgId` counter with `crypto.randomUUID()` in custom-args-tab.tsx to avoid SSR ID conflicts - Add unit tests for custom args passthrough and blocked-arg filtering in both Claude and Gemini arg builders |
||
|
|
c0db3e0e76 |
Revert "feat(selfhost): add single-domain Caddy setup (#899)" (#1062)
This reverts commit
|
||
|
|
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> |
||
|
|
762e64d469 |
fix(agent): restrict custom_env visibility to owner/admin (#1046)
* fix(agent): restrict custom_env visibility to agent owner and workspace admin Agent environment variables (custom_env) were visible to all workspace members, exposing sensitive tokens. Now only the agent owner and workspace owner/admin can view them — regular members receive the field omitted (null) from API responses, and the frontend hides the Environment tab accordingly. Closes #1018 * fix(agent): show masked env keys to non-authorized users instead of hiding tab Instead of completely hiding the Environment tab for non-owner/non-admin users, show the variable keys with masked values (****) in a read-only view. This lets members see which variables are configured without exposing the actual values. - Backend: mask values with "****" instead of nullifying custom_env - Added custom_env_redacted boolean to API response - Frontend: EnvTab supports readOnly mode with lock icon and muted styling |
||
|
|
8030f1adbc |
feat(desktop): restart local daemon when bundled CLI version differs (#1041)
* feat(desktop): restart local daemon when bundled CLI version differs Desktop bundles a multica CLI binary at build time via bundle-cli.mjs. If a local daemon is already running from a previous session with an older CLI, the newly bundled version never takes effect until the user manually restarts. Fix that on the login/auto-start path. - Expose the daemon's CLI version on GET /health as cli_version (sourced from cfg.CLIVersion, which is already set from the ldflag at daemon startup in cmd_daemon.go). - In the desktop main process, query the resolved CLI binary's version once via `multica version --output json` and cache it for the process lifetime. - On daemon:auto-start, if the daemon is already running, compare the two versions. Restart only when BOTH sides are known and the strings differ — a restart kills in-flight agent tasks, so any uncertainty (bundled CLI unknown, older daemon without cli_version field, read failure) fails safe and leaves the daemon alone. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): defer version-mismatch restart until active tasks drain Previous iteration restarted the daemon immediately on a confirmed CLI version mismatch, which would kill any agent tasks mid-execution. Gate the restart on an active-task counter so in-flight work always finishes. - Daemon: add `activeTasks atomic.Int64` on the Daemon struct, increment/decrement it around handleTask, and expose it as `active_task_count` on GET /health. - Desktop: when a version mismatch is confirmed but active_task_count > 0, set a pendingVersionRestart flag instead of restarting. The 5s pollOnce loop retries ensureRunningDaemonVersionMatches on each tick and fires the restart the moment the count drops to 0. - Eventual consistency: if the user keeps the daemon permanently busy, the version stays out of date — that's a strictly better failure mode than silently killing hour-long agent runs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test(daemon): cover version-check decision + /health counter exposure Addresses the test-coverage gap from the second review. - Go: extract the /health handler into a named method `(d *Daemon) healthHandler(startedAt time.Time)` so it can be exercised via httptest without spinning up a listener. Add health_test.go covering cli_version + active_task_count field exposure and the increment / decrement protocol used by pollLoop. - Desktop: extract the pure version-check decision logic into version-decision.ts (no electron, no I/O, no module state). The ensureRunningDaemonVersionMatches wrapper now delegates the "what should we do" decision to decideVersionAction and owns only the side effects (logging, flag mutation, restartDaemon call). - Desktop: bolt vitest onto apps/desktop (vitest.config.ts + catalog devDep + test script) so main-process unit tests have a home. Add version-decision.test.ts covering all four action branches and the busy→idle drain transition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(daemon): bust CLI version cache on retry-install, lock wire-level JSON keys Two polish items from review. - daemon:retry-install now also clears cachedCliBinaryVersion. Previously a retry that landed a newly-downloaded CLI at a different version would false-negative on the next version check because the cached version string was sticky for the process lifetime. - TestHealthHandlerReportsCLIVersionAndActiveTaskCount now decodes into a raw map[string]any and asserts the exact snake_case keys (cli_version, active_task_count, status). The desktop TS client keys on these literal strings, so a silent struct-tag rename must fail the test. Typed struct round-trip kept as a separate value check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2cbebfc568 |
refactor(daemon): remove watch/unwatch workspace logic, default to all workspaces (#1003)
The daemon now automatically watches all workspaces the user belongs to, fetched directly from the API. This removes the manual watch/unwatch workflow, the config-based watched/unwatched lists, the /watch HTTP endpoints, the CLI watch/unwatch commands, and the desktop app's watched workspace UI and reconciliation logic. Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
100146c49e |
feat(selfhost): add single-domain Caddy setup (#899)
* selfhost: add single-domain caddy setup * fix(selfhost): address Caddy review feedback |
||
|
|
4b10c9354a |
fix(fonts): swap Geist Sans → Inter with explicit CJK fallback
Full-width Chinese punctuation (e.g. ,) was rendering at Latin-font metrics, making it look half-width in the editor. Root cause: Geist is Latin-only, and neither web (next/font) nor desktop (@fontsource) declared any CJK fallback, so CJK chars inherited Geist's em-box width through Chromium's per-character fallback. - Web (apps/web/app/layout.tsx): Geist → Inter via next/font/google, with explicit fallback array: system fonts → PingFang SC (macOS) → Microsoft YaHei (Windows) → Noto Sans CJK SC (Linux) → sans-serif. - Desktop: removed @fontsource/geist-sans, added @fontsource-variable/inter (single variable-weight file replaces 4 static weights). Updated --font-sans in globals.css to match web's fallback chain. - Geist Mono kept for code blocks; mono chain has no CJK fallback by design (CJK is non-aligned in mono grids, listing CJK fonts would falsely signal alignment guarantees). Added Consolas to web mono for Windows symmetry with desktop. - Cross-reference sync comments in both layout.tsx and globals.css: CJK tail must stay in sync; Inter primary differs by design (next/font injects `__Inter_xxx` with adjustFontFallback metric override; fontsource uses raw "Inter Variable"). Currently covers English + Simplified Chinese. When ja/ko i18n lands, extend fallback tails with Hiragino Kaku Gothic ProN / Yu Gothic / Apple SD Gothic Neo / Malgun Gothic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
d88fe2608e |
feat(autopilot): scheduled/triggered automations for AI agents (#1028)
* feat(autopilot): add scheduled/triggered automation for AI agents Introduce the Autopilot feature — recurring automations that assign work to AI agents on a schedule or manual trigger. Supports two execution modes: create_issue (creates an issue for the agent to work on) and run_only (directly enqueues an agent task without issue pollution). Backend: migration (3 tables + 2 columns), sqlc queries, AutopilotService with concurrency policies (skip/queue/replace), HTTP CRUD + trigger endpoints, background cron scheduler (30s tick), event listeners for issue→run and task→run status sync. Frontend: types, API client methods, TanStack Query hooks with optimistic mutations, realtime cache invalidation, list page with create dialog, detail page with trigger management and run history, sidebar nav + routes for both web and desktop apps. * feat(autopilot): improve UX — trigger config, edit dialog, template gallery - Replace raw cron input with friendly frequency tabs (Hourly/Daily/Weekdays/Weekly/Custom), time picker, and timezone dropdown defaulting to user's local timezone - Fix Select components showing UUIDs instead of names (Base UI render function pattern) - Add Edit button on detail page opening a unified edit dialog - Remove project/concurrency/issue-title-template from create/edit (simplify for users) - Add trigger configuration inline during autopilot creation - Add template gallery on empty state (6 step-by-step workflow templates) - Rename "Description" to "Prompt" throughout UI - Inject autopilot run timestamp into issue description for agent date awareness - Treat issue status "in_review" as run completion (fixes skip on next trigger) - Make migration idempotent with IF NOT EXISTS clauses |
||
|
|
c79cfaf330 |
fix(auth): honor ?next= redirect through Google OAuth flow (#1024)
The login page now encodes the ?next= param into the Google OAuth state
so the auth callback can redirect to the right destination (e.g.
/invite/{id}) after login, instead of always going to /issues.
|
||
|
|
60c5848794 |
feat(invitation): dedicated /invite/{id} page for accepting invitations (#1023)
The email CTA now deep-links to /invite/{id} instead of the generic app
URL. If the user isn't logged in, they're redirected to login with a
?next= param that brings them back to the invite page.
Changes:
- Backend: GET /api/invitations/{id} endpoint (enriched with workspace/inviter names)
- Backend: Email template now links to /invite/{invitationId}
- Frontend: Shared InvitePage component (packages/views/invite/)
- Frontend: Web route at (auth)/invite/[id], Desktop route at invite/:id
- Frontend: /invite/ excluded from navigation history persistence
|
||
|
|
642c6ae5ee |
docs: add Gemini CLI to all documentation and landing page (#1022)
Gemini CLI support was added to the backend in v0.1.33 but was missing from all user-facing documentation and the website. Added Gemini CLI (and Hermes where missing) to the agents table, quickstart guides, CLI reference, installation docs, self-hosting guide, and landing page hero section with logo. |
||
|
|
c2a5ed73e8 |
fix(web): add /uploads/* rewrite for self-hosted deployments (#1010)
On self-hosted deployments where the frontend is the public entrypoint, uploaded files return 404 because /uploads/* requests aren't proxied to the backend. Add a rewrite rule following the existing pattern for /api/*, /ws, and /auth/*. Closes #1004 |
||
|
|
2a2e6f4746 |
chore(desktop): remove dev:desktop:remote proxy mode
Drops the VITE_REMOTE_API Vite-proxy path introduced in
|
||
|
|
69ef002bbb |
Merge pull request #1000 from multica-ai/fix/desktop-titlebar-drag-region
fix(desktop): stop macOS traffic lights from hijacking titlebar & modals |
||
|
|
7dad45d444 |
feat(desktop): immersive mode hides traffic lights for full-screen modals
Full-screen modals (create-workspace) covered the app titlebar, so the Back button landed on top of the macOS traffic lights — where native hit-test always wins and the button couldn't be clicked. The modal also swallowed the window's drag region. Introduce a desktop IPC channel window:setImmersive that calls BrowserWindow.setWindowButtonVisibility, exposed through the existing desktopAPI preload bridge. A small useImmersiveMode() hook in @multica/views/platform toggles it for the component's lifetime and is a no-op on web / non-macOS. CreateWorkspaceModal now: - calls useImmersiveMode() so traffic lights disappear while it's open - adds a transparent top h-10 drag strip to restore window dragging - moves the Back button from top-6 left-6 to top-12 left-12 with an explicit no-drag region so clicks always reach it Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
7ade4b432d |
fix(desktop): pad main top-bar when sidebar collapses so tabs don't sit under traffic lights
Extract the main-area top bar into a MainTopBar component so it can read sidebar state via useSidebar(). When the sidebar is collapsed, apply pl-20 (80px) to the drag header so the TabBar starts clear of the macOS traffic-light hit-test region (~x=16..68) that always wins over HTML clicks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
cbb2cf0c6c |
chore(desktop): rebuild CLI on every bundle-cli run (#999)
bundle-cli.mjs now invokes `go build` with the same ldflags as `make build` (version/commit/date) before copying the binary into resources/bin/. Running this on every `pnpm dev:desktop`, `dev:remote` and `package` guarantees the bundled CLI matches the current Go source, so you can't accidentally ship a stale binary after editing server/ code. Go's build cache makes no-op builds ~a few hundred ms. Graceful fallback preserved: if `go` is not on PATH (frontend-only contributor), we warn, skip the build, and let cli-bootstrap download the latest release at runtime. Compile errors remain fatal so broken Go code blocks dev rather than silently falling back. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
40aa23a528 |
feat(desktop): daemon management panel with sidebar status bar (#952)
* feat(desktop): add daemon management panel with sidebar status bar Integrate multica daemon lifecycle management into the desktop app so users can start/stop/restart the daemon and view live logs without leaving the UI. Session tokens are automatically synced to the CLI config file, making daemon authentication transparent. - daemon-manager.ts: Electron main process module for daemon lifecycle (health polling, start/stop via CLI, token sync, log tail) - Preload bridge: new daemonAPI with IPC for all daemon operations - Sidebar bottomSlot: persistent daemon status indicator in sidebar footer (desktop-only, injected via AppSidebar slot) - Daemon panel Sheet: right-side drawer with status details, controls, and real-time log viewer with auto-scroll and level coloring - Token sync: on login and app startup, JWT is written to ~/.multica/config.json so daemon can authenticate seamlessly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(desktop): add P1+P2 daemon features — runtimes card, auto-start, settings P1: Runtimes page Local Daemon card - Add topSlot prop to shared RuntimesPage for platform injection - DaemonRuntimeCard shows status, agents, uptime with Start/Stop/ Restart/Logs buttons (desktop-only, injected via slot) P2: Auto-start and auto-stop - Daemon auto-starts on app launch when user is authenticated (controlled by autoStart preference, default: true) - Daemon auto-stops on app quit (controlled by autoStop preference, default: false — daemon keeps running in background by default) - Preferences persisted to ~/.multica/desktop_prefs.json P2: Daemon settings tab - New "Daemon" tab in Settings > My Account section (desktop-only) - Toggle auto-start and auto-stop behavior - CLI installation status check with link to install guide - SettingsPage gains extraAccountTabs prop for platform injection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): address PR review feedback on daemon management Must-fix: - before-quit handler now calls event.preventDefault(), awaits stopDaemon(), then re-calls app.quit() so the daemon actually stops before the app exits - Add concurrency guard (operationInProgress lock) in daemon-manager to reject overlapping start/stop/restart IPC calls - Extract shared types (DaemonState, DaemonStatus, DaemonPrefs), constants (STATE_COLORS, STATE_LABELS), and formatUptime to apps/desktop/src/shared/daemon-types.ts — all renderer components now import from this single source Should-fix: - Log viewer uses monotonic counter (LogEntry.id) instead of array index as React key, preventing full re-renders on overflow - All start/stop/restart handlers now show toast.error() with the error message when the operation fails - startLogTail retries up to 5 times with 2s delay when the log file doesn't exist yet (handles first-run case) Minor: - Cache findCliBinary() result after first successful lookup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(logger): suppress ANSI color codes when stderr is not a TTY Detect whether stderr is connected to a terminal and set tint's NoColor option accordingly. Previously daemon.log files contained raw escape sequences like \033[2m and \033[92m which made them unreadable in the Desktop log viewer and any non-TTY sink (docker logs, systemd, etc). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(daemon): runtime watch/unwatch HTTP endpoints and denylist Add GET/POST/DELETE /watch handlers on the daemon's health port so clients (notably Desktop) can add or remove watched workspaces at runtime without restarting the daemon or editing config.json. Each handler updates in-memory state under d.mu and persists back to ~/.multica/profiles/<name>/config.json for survival across restarts. - CLIConfig gains UnwatchedWorkspaces as an explicit opt-out denylist. syncWorkspacesFromAPI skips entries in the denylist so a manual unwatch isn't silently revived 30s later by the periodic sync. - loadWatchedWorkspaces tolerates an empty config and returns nil instead of erroring out, because Desktop starts daemons with a fresh profile and relies on the sync loop / watch endpoint to populate the list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(desktop): bundled CLI, per-backend profile, and watch UI Make the Desktop app self-sufficient: it bundles its own multica binary, manages its own daemon profile keyed by the backend URL, and authenticates that daemon with a long-lived PAT it mints on first login. The daemon panel gains a checkbox list of watched workspaces and surfaces the active profile + server URL. CLI bootstrap - scripts/bundle-cli.mjs copies server/bin/multica into apps/desktop/resources/bin/ before electron-vite dev and electron-builder package. asarUnpack: resources/** already covers this path, so the binary ships with the .app in prod. - main/cli-bootstrap.ts adds an ensureManagedCli() fallback that downloads the latest release from GitHub when no bundled binary exists (first launch on a machine without developer tooling). - daemon-manager.resolveCliBinary prefers bundled > managed > download > PATH, so local iteration uses the freshly built binary. Daemon profile - resolveActiveProfile now derives a desktop-<host> profile name from the target API URL and creates its config.json on demand. Never reads or writes the user's hand-configured CLI profiles, avoiding the "Desktop polluted my default profile" class of bug. - syncToken detects a JWT input and exchanges it for a PAT via POST /api/tokens; caches the resulting mul_* token in the profile config so subsequent launches skip the round-trip. - startDaemon / stopDaemon / log tail all operate on the resolved profile; renderer sets the target URL via a new daemon:set-target-api-url IPC. Workspace watching - daemon-manager exposes daemon:list-watched / daemon:watch-workspace / daemon:unwatch-workspace IPCs backed by the daemon's new /watch endpoints. - App.tsx reconciles the user's workspace list against the daemon's watched set whenever TanStack Query updates it — new workspaces are registered instantly instead of waiting for the daemon's 30s sync, and removed workspaces are unwatched. - daemon-panel gains a "Watched Workspaces" section with per-workspace checkboxes that call watch/unwatch directly. Opt-outs persist in the profile's unwatched_workspaces denylist. Lifecycle states + UI - DaemonStatus gains `profile`, `serverUrl`, and an `installing_cli` state. Panel shows Profile / Server info rows and a "Setting up…" blurb during first-run CLI download; failure surfaces a Retry button. - Status bar renders a spinner during installation and hides the Start button until setup finishes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): register /onboarding route The create-workspace modal navigates to /onboarding on success, but the Desktop router only had flat routes (issues, projects, runtimes, etc.) — resulting in an "Unexpected Application Error! 404 Not Found" page after creating a new workspace. Mirror the web app's wiring: render OnboardingWizard with onComplete pushing to /issues, via the shared navigation adapter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(desktop): remove sidebar daemon status bar Drop the bottom-left daemon indicator in favor of the DaemonRuntimeCard at the top of the Runtimes page, which already shows the same info plus full Start/Stop/Restart controls and the Logs entry point. A single canonical place avoids fragmenting daemon status across the UI. Also remove the now-unused `bottomSlot` prop from AppSidebar — Desktop was the only consumer, Web never needed it, so keeping it would be dead scaffolding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): daemon panel layout and close button - Logs section now fills the remaining vertical space down to the sheet bottom instead of being capped at h-64, which left a huge empty area below it. Top section (status, actions, watched list) keeps natural height as shrink-0; the watched list gets its own max-h-48 scroll so a long list can't push Logs off screen. - Replace the Sheet's built-in close button with an explicit <button> wired directly to onOpenChange(false). The Base UI Dialog.Close wrapped in Button via the render prop wasn't firing on click in this panel; going straight through the controlled state guarantees it responds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): make daemon panel clickable inside Electron drag region The sheet opens at the top of the window, which visually overlaps the TabBar's -webkit-app-region: drag zone. Even though the sheet portals to document.body, Chromium computes drag regions over the final composited pixels, so the sheet inherited "drag" and swallowed the mouseup of every click (mousedown fired but click never resolved) — including the X close button. Mark the entire SheetContent popup with -webkit-app-region: no-drag to subtract it from the drag region. This also fixes future buttons / checkboxes inside the sheet that would have hit the same issue. While here, move the close button into the SheetHeader as a flex sibling of SheetTitle instead of an absolutely positioned overlay — simpler layout and avoids any stacking-context weirdness. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(desktop): clickable daemon runtime card row The whole Local Daemon row now opens the sheet panel — icon, title, and status line are all part of one click target. This replaces the standalone "Logs" button, which was redundant now that clicking anywhere on the row does the same thing. The right-side action cluster (Start / Stop / Restart) wraps its onClick in stopPropagation so pressing those buttons doesn't bubble up and open the panel. Keyboard access: Enter / Space on the focused row opens the panel, with a focus-visible background for feedback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(runtimes): mark Desktop-launched daemons as managed When the Multica Desktop app spawns the CLI it ships with, the resulting daemon shares its binary with the Electron bundle — Desktop is responsible for updating that binary on every release. Letting the daemon self-update would just get clobbered on the next Desktop launch and could brick the embedded binary mid-update. Propagate a "launched_by" signal end-to-end so the UI can hide the CLI self-update affordance (and the daemon refuses updates as a second line of defense): - Desktop's startDaemon spawns execFile with env MULTICA_LAUNCHED_BY=desktop. - daemon.Config gains LaunchedBy; cmd_daemon reads the env var on boot. - registerRuntimesForWorkspace includes launched_by in the request body. - Server DaemonRegister folds launched_by into runtime.metadata (JSONB — no migration needed). - handleUpdate returns a "failed" status with an explanatory message when LaunchedBy == "desktop", so even a bypass API call can't trigger the self-update path. - RuntimeDetail extracts metadata.launched_by and passes it to UpdateSection, which swaps the Latest / → available / Update button cluster for a muted "Managed by Desktop" label. CLI-only users (brew install, direct tarball) keep the exact same behavior — the env var is empty, the UI shows the update button, the daemon still self-updates on request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): harden daemon manager from PR review - syncToken now takes userId and mints a fresh PAT on user switch, restarting a running daemon so it picks up the new credentials. A .desktop-user-id sidecar in each profile records the owner so a previous user's cached PAT can't be reused on the next login. - App.tsx wires onLogout on CoreProvider to daemonAPI.clearToken() and daemonAPI.stop() so the cached PAT and live daemon don't outlive the session. - startLogTail replaced with a cross-platform watchFile implementation (initial 32 KB window + poll for new bytes, handles truncation). spawn("tail") was broken on Windows. - writeProfileConfig now serializes through a promise chain to prevent concurrent writes from corrupting config.json. - startDaemon keeps the "starting" state until pollOnce confirms /health, avoiding a running → stopped flash when the Go daemon isn't yet listening after the supervisor returns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(desktop): verify downloaded CLI against checksums.txt Download goreleaser's checksums.txt alongside the release archive, parse the sha256 lookup, stream the archive through createHash, and refuse to install on mismatch or missing entry. Closes the supply- chain gap where auto-install would execute an unverified binary on first launch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(desktop): lint and style cleanups from PR review - eslint.config.mjs: add scripts/**/*.{mjs,js} override with globals.node so bundle-cli.mjs lints clean (was erroring on undefined process/console). - daemon-panel.tsx: log level classes now use semantic tokens (text-info, text-warning, text-destructive) instead of hardcoded Tailwind colors; escape the apostrophe in the retry copy. - daemon-settings-tab.tsx: import DaemonPrefs from shared/daemon- types instead of redefining it. - runtimes-page.tsx: fix indentation inside the new topSlot wrapper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Devv <devv@Devvs-Mac-mini.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: yushen <ldnvnbl@gmail.com> |
||
|
|
8748557c7b |
fix: clarify local vs workspace skills to reduce user confusion (#975)
* fix: clarify that local skills work automatically, workspace skills are for team sharing Users were confused, thinking they needed to re-upload locally installed skills (e.g. .claude/skills/) to Multica before agents could use them. Updated UI text across the skills page, agent skills tab, onboarding, and docs to clearly distinguish between local skills (auto-discovered) and workspace skills (for team-wide sharing). Closes #972 * feat(views): replace inline text with info banner for local skills hint The "local runtime skills are always available" message was buried in the description text and easy to miss. Move it into a visible info callout banner with an icon so users notice it immediately. |
||
|
|
609d2e06ae | docs: remove desktop auto-update from v0.1.33 changelog | ||
|
|
7c436c0dcb | docs: add v0.1.33 changelog entry (2026-04-14) | ||
|
|
0a998d1cef |
Merge pull request #846 from multica-ai/agent/j/feb218fd
feat(agent): support custom environment variables for router/proxy mode |