mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
* 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>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { queryOptions } from "@tanstack/react-query";
|
|
import { api } from "../api";
|
|
import type { Agent, Workspace } from "../types";
|
|
|
|
export const workspaceKeys = {
|
|
all: (wsId: string) => ["workspaces", wsId] as const,
|
|
list: () => ["workspaces", "list"] as const,
|
|
members: (wsId: string) => ["workspaces", wsId, "members"] as const,
|
|
invitations: (wsId: string) => ["workspaces", wsId, "invitations"] as const,
|
|
myInvitations: () => ["invitations", "mine"] as const,
|
|
agents: (wsId: string) => ["workspaces", wsId, "agents"] as const,
|
|
skills: (wsId: string) => ["workspaces", wsId, "skills"] as const,
|
|
assigneeFrequency: (wsId: string) => ["workspaces", wsId, "assignee-frequency"] as const,
|
|
};
|
|
|
|
export function workspaceListOptions() {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.list(),
|
|
queryFn: () => api.listWorkspaces(),
|
|
});
|
|
}
|
|
|
|
/** Resolves the workspace whose slug matches, from the cached workspace list. */
|
|
export function workspaceBySlugOptions(slug: string) {
|
|
return queryOptions({
|
|
...workspaceListOptions(),
|
|
select: (list: Workspace[]) => list.find((w) => w.slug === slug) ?? null,
|
|
});
|
|
}
|
|
|
|
export function memberListOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.members(wsId),
|
|
queryFn: () => api.listMembers(wsId),
|
|
});
|
|
}
|
|
|
|
export function agentListOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.agents(wsId),
|
|
queryFn: () =>
|
|
api.listAgents({ workspace_id: wsId, include_archived: true }),
|
|
});
|
|
}
|
|
|
|
export function skillListOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.skills(wsId),
|
|
queryFn: () => api.listSkills(),
|
|
});
|
|
}
|
|
|
|
export function skillDetailOptions(wsId: string, skillId: string) {
|
|
return queryOptions({
|
|
queryKey: [...workspaceKeys.skills(wsId), skillId] as const,
|
|
queryFn: () => api.getSkill(skillId),
|
|
enabled: !!skillId,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Builds a `Map<skillId, Agent[]>` from the cached agent list. The server
|
|
* already returns each agent with its full skill list inline, so no extra
|
|
* request is needed — "which agents use skill X" is pure client-side fold.
|
|
*
|
|
* Exposed as a plain helper rather than a `queryOptions` with `select` so
|
|
* the Map's identity is stable across unrelated agent-cache rerenders —
|
|
* callers wrap this in `useMemo(..., [agents])` and only re-fold when the
|
|
* agent array identity actually changes. Previously this was `{ select }`,
|
|
* which returned a new Map every subscription tick and triggered cascading
|
|
* re-renders on every `agent:updated` WS event.
|
|
*/
|
|
export function selectSkillAssignments(
|
|
agents: Agent[] | undefined,
|
|
): Map<string, Agent[]> {
|
|
const map = new Map<string, Agent[]>();
|
|
if (!agents) return map;
|
|
for (const a of agents) {
|
|
if (a.archived_at) continue;
|
|
for (const s of a.skills ?? []) {
|
|
const existing = map.get(s.id);
|
|
if (existing) existing.push(a);
|
|
else map.set(s.id, [a]);
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
export function invitationListOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.invitations(wsId),
|
|
queryFn: () => api.listWorkspaceInvitations(wsId),
|
|
});
|
|
}
|
|
|
|
export function myInvitationListOptions() {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.myInvitations(),
|
|
queryFn: () => api.listMyInvitations(),
|
|
});
|
|
}
|
|
|
|
export function assigneeFrequencyOptions(wsId: string) {
|
|
return queryOptions({
|
|
queryKey: workspaceKeys.assigneeFrequency(wsId),
|
|
queryFn: () => api.getAssigneeFrequency(),
|
|
});
|
|
}
|