mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 22:17:48 +02:00
* fix(nav): derive route icons from the URL across all nav surfaces (MUL-4370) The same route rendered different icons in the sidebar and the desktop tab bar because the mapping was maintained in three places. Projects had no tab icon at all; autopilots/chat/squads/usage fell back to ListTodo. Establish one contract instead: `@multica/core/paths` maps a route segment to a stable icon *name* (React-free), and `@multica/views/layout` maps that name to a Lucide component. Every nav surface — sidebar, desktop tab bar, and the search palette — resolves through `routeIconForPath(path)`, so a route cannot render two different icons. Crucially the icon is now derived, not stored. `TabSession.icon` is removed, so persisted tab state can no longer hold a stale icon name: a user who had an /autopilots tab from an older build gets the correct icon after upgrade rather than the one that was persisted. Legacy `icon` values in v4 payloads are ignored on rehydration and dropped on the next write. Builds on the design in #5204 by LiangliangSui. Tests: stale/unknown/absent persisted icon on rehydration, derived icon rendering per route in the tab bar, name→component registry totality, and nav-route icon coverage. Co-authored-by: multica-agent <github@multica.ai> * feat(desktop): tab presentation by object identity, not route segment (MUL-4370) Replace the "route segment → icon" tab mapping with a semantic Tab Presentation Contract: a tab's leading visual and title are derived live from its URL + the query cache, so a tab shows *what it points at*, not the module it lives under. - core `parseTabSubject(url)` classifies a URL as page / resource / actor / container (inbox, chat) / flow / unknown, purely (no React, no Lucide). - core `resolveTabPresentation(subject, data)` maps that + cached entity data to a leading visual (issue StatusIcon, ProjectIcon, ActorAvatar, or a type icon) and a title spec. Exhaustive: a new route forces an explicit choice. - views `useTabPresentation` reads the cache (enabled:false, no fetch from the tab bar) and `ResourceLeadingVisual` renders it in a fixed 16×16 slot. - Containers keep their icon; only the title tracks the selection (`/inbox?issue=`, `/chat?session=`), so an inbox-opened issue reads differently from a direct issue tab. - Titles are plain text; the project 📁 and autopilot ⚡ glyphs are dropped from document.title. - Pin no longer replaces the resource visual. Persisted `tab.icon` cleanup from the prior revision is kept; the active tab persists its resolved title as a first-frame fallback (the document.title→observer path is removed). Supersedes the route-segment approach in #5204 per the agreed PRD. No schema, API, or migration changes; reuses existing queries/caches. Tests: table-driven parseTabSubject over every desktop route; the URL/data → visual+title matrix incl. pending/loading, containers, unknown, runtime custom-name; views cache-integration; tab-bar pin + active-tab title persistence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(desktop): archived inbox title sync + attachment filename in tab (MUL-4370) Addresses two PRD gaps from review: 1. Archived Inbox selection now syncs the tab title. `parseTabSubject` captures `?view=archived` on the inbox subject, and the presentation hook resolves the selection against `archivedInboxListOptions` (its own cache, the one the InboxPage populates) instead of only the active list. An `/inbox?view=archived&issue=<id>` tab now shows the archived item's title — issue (`identifier: title`) or non-issue (display title) — and, being purely URL+cache derived, restores correctly on refresh. Previously it fell back to "Inbox" and persisted that wrong title. 2. Attachment tabs use the filename. `parseTabSubject` captures the `?name=` the preview route already carries; the resolver shows the filename as the title and picks a file-type icon from its extension (image/video/audio/ archive/code/text), falling back to the generic File glyph + "Attachment" label only when the name is missing. Tests: parseTabSubject archived/name cases; the presentation matrix for attachment filename/extension + missing fallback and iconForAttachment edge cases; views cache-integration for archived issue/non-issue selection (must resolve against the archived list, not the active one) and attachment filename. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
4.5 KiB
TypeScript
136 lines
4.5 KiB
TypeScript
/**
|
|
* Registry of workspace navigation *pages* and their icons.
|
|
*
|
|
* A "page" here is a collection or tool surface that has no specific resource
|
|
* of its own — Issues, Projects, Settings, etc. Its icon is a stable, static
|
|
* choice keyed by the URL route segment (`/{slug}/{segment}/...`).
|
|
*
|
|
* This is the source of truth the sidebar nav uses (via `resolveRouteIconName`
|
|
* / `routeIconForPath`). The desktop tab bar goes further and derives a full
|
|
* *presentation* (which may be a resource's own icon/avatar/status rather than
|
|
* a page icon) — see `tab-subject.ts` and `tab-presentation.ts`, which build on
|
|
* this registry for the page case.
|
|
*
|
|
* Icon values are *names*, not React components, so this module stays
|
|
* React-free and safe inside `@multica/core`. The name → component registry
|
|
* lives in `packages/views/layout/route-icon-components.tsx`; its
|
|
* `Record<RouteIconName, LucideIcon>` type makes a missing component a compile
|
|
* error.
|
|
*/
|
|
|
|
/** Every icon name a nav page or a tab type-icon can resolve to. */
|
|
export type RouteIconName =
|
|
| "Inbox"
|
|
| "MessageSquare"
|
|
| "CircleUser"
|
|
| "ListTodo"
|
|
| "FolderKanban"
|
|
| "Zap"
|
|
| "Bot"
|
|
| "Users"
|
|
| "BarChart3"
|
|
| "Monitor"
|
|
| "Server"
|
|
| "BookOpenText"
|
|
| "Settings"
|
|
| "File"
|
|
| "FileText"
|
|
| "FileImage"
|
|
| "FileCode"
|
|
| "FileArchive"
|
|
| "FileAudio"
|
|
| "FileVideo"
|
|
| "FileQuestion";
|
|
|
|
/** i18n label key (under the `layout.nav` namespace) for a page. */
|
|
export type NavLabelKey =
|
|
| "inbox"
|
|
| "chat"
|
|
| "my_issues"
|
|
| "issues"
|
|
| "projects"
|
|
| "autopilots"
|
|
| "agents"
|
|
| "squads"
|
|
| "usage"
|
|
| "runtimes"
|
|
| "skills"
|
|
| "settings";
|
|
|
|
/** Stable identifier for each workspace navigation page. */
|
|
export type WorkspacePageKey =
|
|
| "inbox"
|
|
| "chat"
|
|
| "myIssues"
|
|
| "issues"
|
|
| "projects"
|
|
| "autopilots"
|
|
| "agents"
|
|
| "squads"
|
|
| "usage"
|
|
| "runtimes"
|
|
| "skills"
|
|
| "settings";
|
|
|
|
export interface WorkspacePage {
|
|
/** Route segment at index 1 of `/{slug}/{segment}/...`. */
|
|
segment: string;
|
|
/** Static page icon. */
|
|
icon: RouteIconName;
|
|
/** `layout.nav.<navKey>` — the localized page name. */
|
|
navKey: NavLabelKey;
|
|
}
|
|
|
|
/**
|
|
* Single source of truth for workspace nav pages. Keep aligned with the nav
|
|
* destinations in paths.ts and the sidebar nav groups.
|
|
*/
|
|
export const WORKSPACE_PAGES: Record<WorkspacePageKey, WorkspacePage> = {
|
|
inbox: { segment: "inbox", icon: "Inbox", navKey: "inbox" },
|
|
chat: { segment: "chat", icon: "MessageSquare", navKey: "chat" },
|
|
myIssues: { segment: "my-issues", icon: "CircleUser", navKey: "my_issues" },
|
|
issues: { segment: "issues", icon: "ListTodo", navKey: "issues" },
|
|
projects: { segment: "projects", icon: "FolderKanban", navKey: "projects" },
|
|
autopilots: { segment: "autopilots", icon: "Zap", navKey: "autopilots" },
|
|
agents: { segment: "agents", icon: "Bot", navKey: "agents" },
|
|
squads: { segment: "squads", icon: "Users", navKey: "squads" },
|
|
usage: { segment: "usage", icon: "BarChart3", navKey: "usage" },
|
|
runtimes: { segment: "runtimes", icon: "Monitor", navKey: "runtimes" },
|
|
skills: { segment: "skills", icon: "BookOpenText", navKey: "skills" },
|
|
settings: { segment: "settings", icon: "Settings", navKey: "settings" },
|
|
};
|
|
|
|
/** Reverse lookup: route segment → page key. */
|
|
const PAGE_BY_SEGMENT: Record<string, WorkspacePageKey> = Object.fromEntries(
|
|
(Object.keys(WORKSPACE_PAGES) as WorkspacePageKey[]).map((key) => [
|
|
WORKSPACE_PAGES[key].segment,
|
|
key,
|
|
]),
|
|
);
|
|
|
|
/** The page whose route segment is `segment`, or null if none matches. */
|
|
export function pageForSegment(segment: string): WorkspacePageKey | null {
|
|
return PAGE_BY_SEGMENT[segment] ?? null;
|
|
}
|
|
|
|
/** Fallback icon name used when a path's route segment has no explicit page. */
|
|
export const DEFAULT_ROUTE_ICON_NAME: RouteIconName = "ListTodo";
|
|
|
|
/**
|
|
* Resolve the *page* icon name for a workspace-scoped path or full tab URL.
|
|
*
|
|
* Nav and page paths are `/{slug}/{segment}/...`, so the route segment lives
|
|
* at index 1; any search/hash suffix is ignored. Sub-routes keep the parent
|
|
* page's icon. Returns {@link DEFAULT_ROUTE_ICON_NAME} for unknown or
|
|
* too-short paths, so the result is always a renderable name.
|
|
*
|
|
* This is the sidebar/nav entry point. The tab bar does NOT use this for
|
|
* resource detail routes — it resolves a richer presentation instead.
|
|
*/
|
|
export function resolveRouteIconName(path: string): RouteIconName {
|
|
const pathname = path.split(/[?#]/)[0] ?? "";
|
|
const segment = pathname.split("/").filter(Boolean)[1] ?? "";
|
|
const page = pageForSegment(segment);
|
|
return page ? WORKSPACE_PAGES[page].icon : DEFAULT_ROUTE_ICON_NAME;
|
|
}
|