Files
multica/packages/core/paths/paths.ts
Naiyuan Qing 7067d8f125 refactor(skills): redesign list page and add skill detail page (#1607)
* feat(core): add skill detail path and query helpers

- paths.workspace(slug).skillDetail(id) → /:slug/skills/:id
- skillDetailOptions(wsId, skillId) for fetching a single skill
- selectSkillAssignments(agents) folds the cached agent list into
  Map<skillId, Agent[]>; returns a stable reference so consumers can
  memoize against agent-array identity without re-rendering on unrelated
  agent updates

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(views): add cross-platform openExternal helper

On Electron, route through window.desktopAPI.openExternal so the
http/https-only guard in the main process kicks in — direct window.open
inside Electron opens a new renderer window instead of handing the URL
to the OS shell. On web, fall back to window.open with noopener+noreferrer.
SSR-safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): extract edit-permission hook and origin helper

- use-can-edit-skill: mirrors the server's rule (admin/owner ∨ creator)
  so the UI can hide/disable actions instead of waiting for a 403. Takes
  wsId explicitly per the repo rule for workspace-aware hooks.
- lib/origin: discriminated view over Skill.config.origin (manual /
  runtime_local / clawhub / skills_sh) so consumers don't spread JSONB
  parsing across the UI tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(skills): rewrite skills list page and collapse import UI

- SkillsPage rewritten: new hero header, single table layout with
  columns (Name / Used by / Source · Added by / Updated), agent avatar
  stack per skill, filter tabs aligned with Issues/MyIssues header
  (Button variant=outline + Tooltip + bg-accent active state).
- CreateSkillDialog: dedicated dialog for the manual/import entry
  points, replaces the inline row-triggered dialog.
- runtime-local import: dialog variant deleted; panel is now the single
  entry point, embeddable inside CreateSkillDialog. Panel covered by a
  new test.
- Deleted runtime-local-skill-row (no longer needed — row rendering
  lives in SkillsPage directly) and the old skills-page.test.tsx
  (structure diverged beyond salvaging; will be re-added alongside the
  detail-page tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(skills): add skill detail page and wire routes on web and desktop

- SkillDetailPage: dedicated view for a single skill (name, description,
  origin, assignments, file listing). Uses skillDetailOptions and the
  new origin / use-can-edit-skill helpers.
- apps/web: /:workspaceSlug/skills/:id Next.js route.
- apps/desktop: /:slug/skills/:id added to the memory router under
  WorkspaceRouteLayout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(skills): bump runtime-local-skill-import-panel timeouts for CI

The test chains a five-step async cascade (runtime list → setSelectedRuntimeId
effect → skills query → auto-select effect → row render). Comfortable on
local (~600ms) but tight against RTL's 1 s default on CI where jsdom +
Vitest import takes ~100s. Bump findByText and the two waitFor calls to
5 s each — no production behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:51:58 +08:00

61 lines
2.3 KiB
TypeScript

/**
* Centralized URL path builder. All navigation in shared packages (packages/views)
* MUST go through this module — no hardcoded string paths.
*
* Two kinds of paths:
* - workspace-scoped: paths.workspace(slug).xxx() — carry workspace in URL
* - global: paths.login(), paths.newWorkspace(), paths.invite(id) — pre-workspace routes
*
* Why pure functions + builder pattern:
* - Changing a route shape (e.g. adding workspace slug prefix) becomes a single-file edit
* - IDs are always URL-encoded here so callers can't forget
* - Zero runtime deps means this module is safe in Node (tests) and browsers
*/
const encode = (id: string) => encodeURIComponent(id);
function workspaceScoped(slug: string) {
const ws = `/${encode(slug)}`;
return {
root: () => `${ws}/issues`,
issues: () => `${ws}/issues`,
issueDetail: (id: string) => `${ws}/issues/${encode(id)}`,
projects: () => `${ws}/projects`,
projectDetail: (id: string) => `${ws}/projects/${encode(id)}`,
autopilots: () => `${ws}/autopilots`,
autopilotDetail: (id: string) => `${ws}/autopilots/${encode(id)}`,
agents: () => `${ws}/agents`,
inbox: () => `${ws}/inbox`,
chat: () => `${ws}/chat`,
myIssues: () => `${ws}/my-issues`,
runtimes: () => `${ws}/runtimes`,
skills: () => `${ws}/skills`,
skillDetail: (id: string) => `${ws}/skills/${encode(id)}`,
settings: () => `${ws}/settings`,
};
}
export const paths = {
workspace: workspaceScoped,
// Global (pre-workspace) routes
login: () => "/login",
newWorkspace: () => "/workspaces/new",
invite: (id: string) => `/invite/${encode(id)}`,
onboarding: () => "/onboarding",
authCallback: () => "/auth/callback",
root: () => "/",
};
export type WorkspacePaths = ReturnType<typeof workspaceScoped>;
// Prefixes — not slug names — because we match against full URL paths.
// A path is global if it equals or begins with any of these.
// Note: `/workspaces/` (trailing slash) is the prefix — `workspaces` is reserved,
// so any path starting with `/workspaces/...` is system-owned, not user-owned.
const GLOBAL_PREFIXES = ["/login", "/workspaces/", "/invite/", "/onboarding", "/auth/", "/logout", "/signup"];
export function isGlobalPath(path: string): boolean {
return GLOBAL_PREFIXES.some((p) => path === p || path.startsWith(p));
}