Files
multica/packages/views/editor/utils/link-handler.ts
Bohan Jiang 451c46c43f refactor(usage): rename Dashboard → Usage + dynamic per-agent leaderboard (#2511)
The page added in #2462 lived at `/{slug}/dashboard` and was titled
"Dashboard", which collides with the conventional meaning ("personal
landing surface") and doesn't tell new users what the page is for. Its
actual contents — token spend, cost, run time, task counts — map cleanly
onto the OpenAI / Anthropic / Vercel "Usage" surface, so rename to that.

Renames (user-visible)
- Route: `/{slug}/dashboard` → `/{slug}/usage` (web App Router + desktop
  memory router)
- Sidebar entry: label "Dashboard" / "看板" → "Usage" / "用量", icon
  LayoutDashboard → BarChart3 (page header icon swapped in sync)
- Page title in en/zh-Hans
- Reserved-slugs: add `usage` to workspace route segments group;
  `dashboard` stays reserved in the marketing group (back-compat against
  workspace slug collisions + keeps the name free for a future Home page)
- i18n namespace `dashboard` → `usage` across resources-types.ts,
  locales/index.ts, and the moved JSON files
- WORKSPACE_ROUTE_SEGMENTS in editor link-handler
- paths.workspace(slug).dashboard() → .usage(), with matching test
  expectation updates

Per-agent leaderboard polish (`packages/views/dashboard/components/
dashboard-page.tsx`)
- Card title "Cost & run time by agent" → "Leaderboard" with a 4-way
  Segmented control: Tokens / Cost / Time / Tasks
- Active metric drives row order, progress-bar width, and the
  emphasised column header / cell — keeping ranking, visual quantity,
  and column emphasis in lockstep so users always see what's being
  measured
- Default sort = Tokens (most universally meaningful; Cost still one
  click away)
- Project filter dropdown:
  - Show ProjectIcon next to the selected project + each list item;
    FolderKanban as the "All projects" fallback (matches ProjectPicker
    language)
  - alignItemWithTrigger={false} so "All projects" doesn't get pushed
    above the trigger and clipped when the header sits at the top of
    the viewport (was the root cause of "can't re-select All projects"
    once a project was selected)
  - max-h-72 to cap the dropdown when workspaces accrue many projects;
    matches the runtime-detail Select precedent
- Folder name `packages/views/dashboard/*` and `DashboardPage`
  component name intentionally left in place — user-visible rename
  only, no broad code refactor.

Old `/dashboard` routes are not redirected because the page only landed
in #2462 (a few days ago); no real users, external links, or
desktop-tab persistence have settled on it yet.
2026-05-13 14:07:53 +08:00

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([
"usage",
"issues",
"projects",
"autopilots",
"agents",
"inbox",
"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://");
}