mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 06:28:23 +02:00
* feat(chat): Chat V2 — sidebar entry + main-area page Replace the floating drawer + FAB with a first-class workspace route `/:slug/chat`. Sidebar gets a single `Chat` entry under Inbox with an unread dot; session history lives inside the Chat tab via a popover rather than leaking into the global sidebar (keeps Multica's "nouns in the nav" semantic — Inbox / Issues / Projects are work objects, Chat is a tool). - Add `paths.workspace(slug).chat()` + update link-handler route set. - New `ChatPage` view with PageHeader, history popover, centered messages/composer column, and empty-state starter prompts. - Delete `ChatWindow`, `ChatFab`, resize helpers, and standalone `ChatSessionHistory` (history now embedded in the popover). - Drop `isOpen`/`toggle`/`showHistory`/resize fields from `useChatStore` — the page is a route now, not an overlay. - Wire the new `/chat` route on web (App Router) and desktop (react-router + tab-store icon mapping). Addresses MUL-1322. * fix(chat): align composer width with message column The ChatPage wrapper added px-4 on top of ChatInput's own px-5, making the composer 32px narrower than the messages column. Drop the outer px-4 so both share the same max-w-3xl outer + px-5 inner padding provided by ChatMessageList / ChatInput. * fix(chat): taller default composer (~3 lines visible, 8 max) min-h 4rem → 7rem, max-h 10rem → 15rem. Empty state previously showed only 1 text row after pb-9 for the action bar; raise the floor so there's visible writing room and lift the ceiling so a longer draft can grow before scrolling kicks in. * fix(chat): restore anchor + in-flight indicator + cold-start session restore Three issues surfaced by review: 1. ContextAnchorButton always disabled on /:slug/chat — useRouteAnchorCandidate only matches issue/project/inbox pathnames, so moving chat to its own route dropped 'bring the page I was on into the conversation'. Track the last anchor-eligible location globally (new useAnchorTracker mounted in AppSidebar + lastAnchorLocation on useChatStore) and substitute it when on /chat. 2. No global 'Multica is working' cue after ChatFab deletion. Subscribe the sidebar Chat entry to pendingChatTasksOptions and swap the unread dot for a spinner while any chat task is in flight. 3. ChatPage restore effect latched didRestoreRef before the sessions query resolved, so cold-start direct nav to /chat landed on the empty state even when the server had an active session. Wait for isSuccess before locking the ref. * fix(chat): clear lastAnchorLocation on workspace rehydration The pathname captured in workspace A would otherwise be reused against workspace B's wsId, triggering a cross-workspace issue/project fetch and silently leaking anchor context into chat messages. --------- Co-authored-by: Lambda <f252c2c5-7d1d-4f3c-b394-a61abfe673fc@users.noreply.multica.ai>
67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
/**
|
|
* Shared link handling utilities for the editor system.
|
|
*
|
|
* Used by content-editor (ProseMirror click handler), readonly-content
|
|
* (react-markdown link component), and link-hover-card (Open button).
|
|
*/
|
|
|
|
import { isGlobalPath } from "@multica/core/paths";
|
|
|
|
/**
|
|
* Top-level workspace-scoped routes. Used to detect "/{route}/..." paths that
|
|
* were authored without a workspace slug — we prepend the current slug so they
|
|
* resolve correctly under the new /{slug}/{route}/... URL shape.
|
|
*
|
|
* Why a hardcoded allowlist: the heuristic must be conservative. A path like
|
|
* "/acme/issues/abc" already has a slug (first segment "acme" isn't a known
|
|
* route), so leaving it alone is correct. A path like "/foo/bar" where "foo"
|
|
* isn't a known route is ambiguous — we don't rewrite it, treating the author
|
|
* as intentional. Only "/issues/..." style paths get auto-prefixed.
|
|
*/
|
|
const WORKSPACE_ROUTE_SEGMENTS = new Set([
|
|
"issues",
|
|
"projects",
|
|
"autopilots",
|
|
"agents",
|
|
"inbox",
|
|
"chat",
|
|
"my-issues",
|
|
"runtimes",
|
|
"skills",
|
|
"settings",
|
|
]);
|
|
|
|
/**
|
|
* Open a link — internal paths dispatch multica:navigate, external open new tab.
|
|
*
|
|
* If `currentSlug` is provided and `href` is a workspace-scoped path lacking a
|
|
* slug (e.g. "/issues/abc" instead of "/{slug}/issues/abc"), the slug is
|
|
* prepended. This is for legacy markdown content authored before the URL
|
|
* refactor, or future content where users forget the slug when pasting.
|
|
*/
|
|
export function openLink(href: string, currentSlug?: string | null): void {
|
|
if (href.startsWith("/")) {
|
|
let path = href;
|
|
if (currentSlug && !isGlobalPath(path)) {
|
|
const firstSegment = path.split("/")[1];
|
|
if (firstSegment && WORKSPACE_ROUTE_SEGMENTS.has(firstSegment)) {
|
|
// Path looks like /issues/abc (no slug) — prepend current slug.
|
|
path = `/${currentSlug}${path}`;
|
|
}
|
|
// Otherwise the first segment is either already a slug (e.g. "acme" in
|
|
// "/acme/issues") or something unknown (e.g. "/foo"). Leave it alone —
|
|
// the user wrote what they meant.
|
|
}
|
|
window.dispatchEvent(
|
|
new CustomEvent("multica:navigate", { detail: { path } }),
|
|
);
|
|
} else {
|
|
window.open(href, "_blank", "noopener,noreferrer");
|
|
}
|
|
}
|
|
|
|
/** Check if a href is a mention protocol link (should not be opened as a regular link). */
|
|
export function isMentionHref(href: string | null | undefined): href is string {
|
|
return !!href && href.startsWith("mention://");
|
|
}
|