* fix(issues): return to the source list after deleting an issue
Deleting an issue from its detail page always pushed the workspace
Issues list, so opening an issue from My Issues (or a project list,
search, a pin, an agent panel) and deleting it dropped the user's
navigation context — GH #5995.
Go back instead. `useBackOrReplace` steps back when the platform
reports in-app history and replaces with a fallback path when there
is none, so a shared link opened cold or a new tab never steps off
the app. Web answers via the Navigation API, falling back to counting
its own pushes; desktop reads the active tab's virtual history.
`replace`, not `push`: the deleted issue's URL must not stay in
history for the back button to land on a 404.
The not-found "Back to Issues" button loses the same context, so it
moves to the same helper and its label becomes a plain "Back".
Co-authored-by: multica-agent <github@multica.ai>
* fix(web): track history position, not push count, for canGoBack
The Navigation API fallback only ever counted pushes, so `pushes > 0`
did not mean the current entry still had an in-app page behind it.
Cold-open an issue, click Issues, press the browser's Back button, then
delete: the tracker still claimed history and `back()` would step off
Multica — the exact case the fallback exists to prevent.
Count depth instead: a push adds one, any traversal takes one away
(clamped at zero). Reaching the document's first entry requires
traversing back at least as many times as we pushed, so a positive
depth can never be claimed while sitting on it. A browser Forward is
now conservative — it reports no history where a step back would have
been fine — which costs a fallback navigation rather than an exit.
Also corrects the useBackOrReplace contract comment: stepping back
leaves the dead URL in forward history, so the guarantee is that it
never lands on the back stack, not that it leaves history entirely.
Co-authored-by: multica-agent <github@multica.ai>
* fix(web): answer canGoBack from the browser alone, never from push counts
Review found the counter still lied, one level deeper: it counted
`router.push` calls, and a call is not a committed history entry. Next
drops a push to `replaceState` when the canonical URL is unchanged
(app-router.js, the `pendingPush && href !== canonicalUrl` branch), so
clicking a self-link — the breadcrumb on an issue detail page, a pin to
the issue you are on — incremented depth with no entry behind it, and a
delete from there could still step off the app.
Fixing the count needs per-entry markers, which means depending on both
React effect ordering (our provider's effect runs before app-router's
history commit) and Next's own preserveCustomHistoryState behaviour.
Two rounds of review have now found holes in hand-rolled history
tracking; a third layer of it is not the way to buy this guarantee.
So stop deriving. `canGoBack` is the Navigation API's answer or `false`.
Browsers without it take the fallback path, which is exactly what they
did before any of this existed — nobody regresses, and the "wrong true
walks the user out of the app" failure is now unreachable by
construction.
Drops the tracker, the popstate wiring and the push wrapper: the
`multica:navigate` bridge returns to its original shape.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Bohan-J <bohan@devv.ai>
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): open in-app links in a tab instead of the browser (MUL-5208)
A link written as an absolute URL on this deployment's own origin
(`https://<app-host>/acme/issues/1` — what "copy link" produces, and what
agents paste into chat) fell through openLink's external branch to
window.open, which Electron routes to shell.openExternal. Clicking an issue
link in Desktop chat therefore opened a browser window instead of a tab.
openLink now resolves such a URL back to its in-app path and takes the same
route a relative path does. Backend-served prefixes (/api/, /_next/) stay
external so attachment downloads keep working.
Two supporting fixes the change depends on:
- The `multica:navigate` event had no listener on web, so in-app paths in
content were dead links there; normalizing app URLs would have extended
that to every pasted app URL. The web platform layer now answers the event
with a router push.
- The desktop handler opened every path inside the active workspace's tab
group. A cross-workspace link now goes through switchWorkspace, matching
what the navigation adapter already does for pushes.
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): scope in-app link conversion to workspace pages, answer it in issue windows
Review follow-ups on MUL-5208.
1. The non-page exclusion list only covered /api/ and /_next/, so a
same-origin /uploads/* link — local-storage attachments, served by the
backend and proxied by web — was routed as an app page, opening a dead tab
instead of the file. Replaced the deny-list with the app's own routing
model: an absolute URL converts to an in-app path only when its first
segment is a slug a workspace could own, which the existing reserved-slug
list (shared with the backend) already answers. /api, /uploads, /_next,
/favicon.ico and the pre-workspace routes all stay external without a
second list to keep in sync.
2. A dedicated issue window derives the same app origin from its adapter but
had no multica:navigate listener, so a same-origin link there became a
silent no-op (it used to reach the browser). The window now answers the
event: another issue opens in place — matching what its adapter push and
mention chips already do — and any other app page, which this single-route
window cannot host, goes to the browser.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <agent@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
Internal navigation on web feels laggy because clicking a sidebar link blocks
0.2–0.6s with zero visual feedback — no prefetch, no Suspense fallback in the
dashboard segment, and no React transition to mark the route commit as pending.
This change adds the three pieces App Router needs to make the click→commit
window feel instant, scoped to the (dashboard) segment so auth/landing keep
their existing chrome:
- NavigationAdapter gains an optional prefetch(path). The web adapter wires
it to router.prefetch; desktop leaves it undefined (react-router has no
equivalent and doesn't need one). AppLink prefetches on hover/focus and
preserves caller-supplied onMouseEnter/onFocus/onClick.
- NavigationProvider wraps push/replace in useTransition and exposes the
pending flag via useIsNavigating(). Every useNavigation().push caller —
sidebar AppLink, command palette, post-create modal jumps — picks this up
automatically.
- New apps/web/app/[workspaceSlug]/(dashboard)/loading.tsx renders a minimal
skeleton during cold transitions inside the dashboard segment only.
- DashboardLayout renders a 1px top progress bar driven by useIsNavigating.
packages/views remains free of next/* imports; desktop is unaffected by
construction (no prefetch, transition flips quickly, no loading.tsx).
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): derive appUrl from apiUrl in dev so copy-link follows the connected env
Local desktop dev was hardcoding appUrl to http://localhost:3000, so the
"Copy issue link" output pointed at localhost even when the renderer was
connected to a remote (e.g. test) backend — the resulting URL only worked
on the developer's machine.
- runtime-config dev path now mirrors the production loader: when
VITE_APP_URL is unset, derive appUrl from apiUrl (host-only). The
localhost api host is special-cased to keep the local web port (3000),
while a remote api host (api.test.x) yields a remote appUrl.
- Web navigation adapter now implements getShareableUrl directly with
window.location.origin instead of leaving it undefined.
- NavigationAdapter.getShareableUrl is now required; copyLink callers
drop the window.location fallback branch and call it unconditionally.
- Add the missing getShareableUrl mock in issue-detail.test.tsx.
Co-authored-by: multica-agent <github@multica.ai>
* fix(desktop): strip leading api. label when deriving appUrl
Address Emacs' code review on PR #2298. The previous derivation kept the
api hostname unchanged, so VITE_API_URL=https://api.test.multica.ai
produced appUrl=https://api.test.multica.ai — not the env's actual web
URL. Multica's convention exposes the api at api.<web-host>; strip that
leading label (when the host has at least 3 labels, to avoid mangling
short hosts like api.local) so a single api configuration produces the
correct shareable web origin.
- api.multica.ai → multica.ai
- api.test.multica.ai → test.multica.ai
- api-staging.x.com → unchanged (no leading "api." label)
- congvc-x99.ts.net → unchanged
Update both the dev and production tests; also fix the existing
runtime-config-loader test that asserted the unstripped value.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
P0: Replace all localStorage calls in packages/core with StorageAdapter
- Create StorageAdapter interface (getItem/setItem/removeItem)
- Auth store factory now requires storage parameter
- Workspace store factory accepts optional storage parameter
- WSProvider accepts storage prop for token retrieval
- apps/web/platform/ passes localStorage as the web implementation
P1: Remove sonner UI dependency from packages/core
- Replace toast.error() in workspace store with onError callback
- Move sonner import to apps/web/platform/workspace.ts
- Remove sonner from packages/core/package.json dependencies
P2: Delete 5 pure re-export barrel files in apps/web/features/
- features/issues/index.ts, modals/index.ts, navigation/index.ts,
workspace/index.ts, inbox/index.ts — all had zero consumers
- features/ now only contains auth/ (web-only cookie + initializer)
and landing/ (web-only pages)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>