Open catalog/bundled skills as a regular page, not a modal (PAP-10907)

Round-3 routed catalog/optional/bundled cards to the install dialog, which
the user reported as a "weird modal." Catalog skills now open a standalone
full page in the new store instead:

- Clicking a catalog/bundled/optional card opens /skills?catalog=<ref>, a
  regular full page (back-to-store link + Files tree + skill body + Install
  CTA) rather than the install modal or the legacy split view.
- The install confirmation modal now only appears on an explicit Install click.
- Catalog detail/file queries are keyed off the selected catalog ref instead
  of the removed view=catalog tab; legacy ?view=catalog deep links still
  redirect onto the new surface.
- Removed the now-unused setViewParam helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-06-11 16:18:33 +00:00
1 parent c9b1fb82d1
commit cad64d2280
1 file changed
+68 -268
+68 -268
View File
@@ -77,6 +77,7 @@ import {
Boxes,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
Code2,
Download,
@@ -3647,15 +3648,6 @@ export function CompanySkills() {
});
}
function setViewParam(view: "installed" | "catalog") {
setSearchParams((current) => {
const next = new URLSearchParams(current);
if (view === "installed") next.delete("view");
else next.set("view", "catalog");
return next;
});
}
function setSourceFilter(next: SourceFilter) {
setSearchParams((current) => {
const params = new URLSearchParams(current);
@@ -3688,16 +3680,15 @@ export function CompanySkills() {
]);
}, [routeSkillToken, setBreadcrumbs]);
// The old split catalog view no longer exists — bundled/catalog skills live in
// the discovery grid and install dialog now. Strip legacy `view=catalog` /
// `catalog` params so stale deep links land on the new surface (PAP-10907).
// The old split catalog view no longer exists — catalog/bundled skills now open
// as a regular full page keyed by `?catalog=<ref>`. Strip the legacy `view`
// param so stale `?view=catalog` deep links land on the new surface (PAP-10907).
useEffect(() => {
if (!searchParams.has("view") && !searchParams.has("catalog")) return;
if (!searchParams.has("view")) return;
setSearchParams(
(current) => {
const next = new URLSearchParams(current);
next.delete("view");
next.delete("catalog");
return next;
},
{ replace: true },
@@ -4027,14 +4018,14 @@ export function CompanySkills() {
const catalogDetailQuery = useQuery({
queryKey: queryKeys.companySkills.catalogDetail(selectedCatalogRef ?? ""),
queryFn: () => companySkillsApi.catalogDetail(selectedCatalogRef!),
enabled: Boolean(selectedCompanyId && selectedCatalogRef && activeView === "catalog"),
enabled: Boolean(selectedCompanyId && selectedCatalogRef),
staleTime: 60_000,
});
const catalogFileQuery = useQuery({
queryKey: queryKeys.companySkills.catalogFile(selectedCatalogRef ?? "", catalogSelectedPath),
queryFn: () => companySkillsApi.catalogFile(selectedCatalogRef!, catalogSelectedPath),
enabled: Boolean(selectedCompanyId && selectedCatalogRef && activeView === "catalog" && catalogSelectedPath),
enabled: Boolean(selectedCompanyId && selectedCatalogRef && catalogSelectedPath),
staleTime: 60_000,
});
@@ -4143,7 +4134,6 @@ export function CompanySkills() {
pushToast({ tone: "warn", title: "Install warnings", body: result.warnings[0] });
}
if (result.action === "created") {
setViewParam("installed");
navigate(routeForSkill(result.skill));
}
},
@@ -4293,19 +4283,16 @@ export function CompanySkills() {
importSkill.mutate(trimmedSource);
}
// Opening a card stays inside the new store: installed skills go to their
// detail route; catalog/bundled skills open the install dialog inline. The
// legacy split catalog view is gone (PAP-10907).
// Opening a card stays inside the new store and always lands on a regular full
// page: installed skills go to their detail route; catalog/bundled/optional
// skills open the standalone catalog page (no modal, no legacy split view).
function openDiscoveryCard(card: DiscoveryCard) {
if (card.skillId) {
navigate(routeForSkillId(card.skillId));
return;
}
if (card.catalogRef) {
const catalogSkill = (catalogListQuery.data ?? []).find(
(entry) => entry.id === card.catalogRef || entry.key === card.key,
);
if (catalogSkill) openInstallDialog(catalogSkill);
selectCatalog(card.catalogRef);
}
}
@@ -4568,257 +4555,70 @@ export function CompanySkills() {
onDelete={openDeleteDialog}
deletePending={deleteSkill.isPending}
/>
) : (
<div className="flex min-h-[calc(100vh-12rem)] flex-col">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 pt-3 pb-[5px]">
<Tabs value={activeView} onValueChange={(value) => setViewParam(value === "catalog" ? "catalog" : "installed")}>
<TabsList variant="line" className="p-0">
<TabsTrigger value="installed" className="px-3">
<span>Installed</span>
<span className="ml-1.5 text-[11px] text-muted-foreground">{installedSkills.length}</span>
</TabsTrigger>
<TabsTrigger value="catalog" className="px-3">
<span>Catalog</span>
<span className="ml-1.5 text-[11px] text-muted-foreground">{catalogListQuery.data?.length ?? 0}</span>
</TabsTrigger>
</TabsList>
</Tabs>
<div className="flex items-center gap-2">
{activeView === "installed" ? (
<Button
variant="ghost"
size="icon-sm"
onClick={() => scanProjects.mutate()}
disabled={scanProjects.isPending}
title="Scan project workspaces for skills"
>
<RefreshCw className={cn("h-4 w-4", scanProjects.isPending && "animate-spin")} />
</Button>
) : null}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="default">
<Plus className="mr-1 h-3.5 w-3.5" />
Add skill
<ChevronDown className="ml-1 h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={() => setViewParam("catalog")}>
<Boxes className="mr-2 h-4 w-4" />
Browse catalog
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
setViewParam("installed");
setEmptySourceHelpOpen(true);
}}
>
<Globe className="mr-2 h-4 w-4" />
Import from URL or path
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
setViewParam("installed");
openCreateWizard();
}}
>
<Pencil className="mr-2 h-4 w-4" />
Create blank skill
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : selectedCatalogRef ? (
// Catalog / optional / bundled skills open as a regular full page in the
// new store — no modal, no legacy split view (PAP-10907).
<div className="min-h-[calc(100vh-12rem)]">
<div className="border-b border-border px-4 py-3">
<Link
to="/skills"
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground no-underline transition-colors hover:text-foreground"
>
<ChevronLeft className="h-4 w-4" />
Back to store
</Link>
</div>
</div>
{activeView === "installed" ? (
<div className="grid flex-1 gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="border-r border-border">
<div className="border-b border-border px-4 py-3">
<div className="flex items-center gap-2 border-b border-border pb-2">
<Search className="h-4 w-4 text-muted-foreground" />
<input
value={skillFilter}
onChange={(event) => setSkillFilter(event.target.value)}
placeholder="Filter skills"
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<SourceFilterMenu counts={sourceCounts} value={sourceFilter} onChange={setSourceFilter} />
</div>
<div className="mt-3 flex items-center gap-2 border-b border-border pb-2">
<input
value={source}
onChange={(event) => setSource(event.target.value)}
placeholder="Paste path, GitHub URL, or skills.sh command"
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<Button
size="sm"
variant="ghost"
onClick={handleAddSkillSource}
disabled={importSkill.isPending}
>
{importSkill.isPending ? <RefreshCw className="h-4 w-4 animate-spin" /> : "Add"}
</Button>
</div>
{scanStatusMessage && (
<p className="mt-3 text-xs text-muted-foreground">{scanStatusMessage}</p>
)}
</div>
{skillsQuery.isLoading ? (
<PageSkeleton variant="list" />
) : skillsQuery.error ? (
<div className="px-4 py-6 text-sm text-destructive">{skillsQuery.error.message}</div>
) : installedSkills.length === 0 ? (
<div className="px-4 py-8">
<EmptyState
icon={Boxes}
message="No skills installed yet."
/>
<div className="mt-3 flex flex-col items-center gap-2">
<Button size="sm" onClick={() => setViewParam("catalog")}>
<Boxes className="mr-1.5 h-3.5 w-3.5" /> Browse catalog
</Button>
<Button size="sm" variant="ghost" onClick={() => setEmptySourceHelpOpen(true)}>
Import from URL
</Button>
</div>
</div>
) : (
<SkillList
skills={installedSkills}
selectedSkillId={selectedSkillId}
skillFilter={skillFilter}
sourceFilter={sourceFilter}
expandedSkillId={expandedSkillId}
expandedDirs={expandedDirs}
selectedPaths={selectedSkillId ? { [selectedSkillId]: selectedPath } : {}}
onToggleSkill={(currentSkillId) =>
setExpandedSkillId((current) => current === currentSkillId ? null : currentSkillId)
}
onToggleDir={(currentSkillId, path) => {
setExpandedDirs((current) => {
const next = new Set(current[currentSkillId] ?? []);
if (next.has(path)) next.delete(path);
else next.add(path);
return { ...current, [currentSkillId]: next };
});
}}
onSelectSkill={(currentSkillId) => setExpandedSkillId(currentSkillId)}
onSelectPath={() => {}}
onClearFilters={() => setSourceFilter("all")}
/>
)}
</aside>
<div className="min-w-0 pl-6">
<SkillPane
loading={skillsQuery.isLoading || detailQuery.isLoading}
detail={activeDetail}
file={activeFile}
fileLoading={fileQuery.isLoading && !activeFile}
updateStatus={updateStatusQuery.data}
updateStatusLoading={updateStatusQuery.isLoading}
viewMode={viewMode}
editMode={editMode}
draft={draft}
setViewMode={setViewMode}
setEditMode={setEditMode}
setDraft={setDraft}
onCheckUpdates={() => {
void updateStatusQuery.refetch();
}}
checkUpdatesPending={updateStatusQuery.isFetching}
onInstallUpdate={() => installUpdate.mutate()}
installUpdatePending={installUpdate.isPending}
onDelete={openDeleteDialog}
deletePending={deleteSkill.isPending}
onSave={() => saveFile.mutate()}
savePending={saveFile.isPending}
attachAgents={eligibleAgentsForAttach}
versions={versionsQuery.data ?? []}
onSubmitAttach={handleAttachSubmit}
attachPending={attachAgentsMutation.isPending}
/>
</div>
</div>
) : (
<div className="grid flex-1 gap-0 xl:grid-cols-[19rem_minmax(0,1fr)]">
<aside className="border-r border-border">
<div className="border-b border-border px-4 py-3">
<div className="flex items-center gap-2 border-b border-border pb-2">
<Search className="h-4 w-4 text-muted-foreground" />
<input
value={catalogFilter}
onChange={(event) => setCatalogFilter(event.target.value)}
placeholder="Search catalog"
className="w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<CatalogFilterMenu
kindFilter={catalogKindFilter}
categoryFilter={catalogCategoryFilter}
categories={catalogCategories}
onKindChange={setCatalogKindFilter}
onCategoryChange={setCatalogCategoryFilter}
/>
</div>
</div>
{catalogListQuery.isLoading ? (
<PageSkeleton variant="list" />
) : catalogListQuery.error ? (
<div className="px-4 py-6 text-sm text-destructive">{catalogListQuery.error.message}</div>
) : (
<CatalogList
skills={catalogListQuery.data ?? []}
kindFilter={catalogKindFilter}
categoryFilter={catalogCategoryFilter}
catalogFilter={catalogFilter}
installedByKey={installedByKey}
selectedCatalogRef={selectedCatalogRef}
{catalogListQuery.isLoading || catalogDetailQuery.isLoading ? (
<PageSkeleton variant="detail" />
) : !selectedCatalogSkill ? (
<EmptyState icon={Boxes} message="Catalog skill not found." />
) : (
<div className="grid gap-0 xl:grid-cols-[14rem_minmax(0,1fr)]">
<aside className="border-b border-border px-3 py-4 xl:border-b-0 xl:border-r">
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">Files</div>
<SkillTree
nodes={buildTree(selectedCatalogSkill.files.map((file) => ({ path: file.path, kind: file.kind })))}
skillId={selectedCatalogSkill.id}
selectedPath={catalogSelectedPath}
expandedSkillId={expandedCatalogSkillId}
expandedDirs={expandedCatalogDirs}
onSelect={selectCatalog}
onSelectPath={selectCatalog}
onToggleSkill={(catalogRef) =>
setExpandedCatalogSkillId((current) => current === catalogRef ? null : catalogRef)
}
onToggleDir={(catalogRef, path) => {
expandedDirs={expandedCatalogDirs[selectedCatalogSkill.id] ?? new Set<string>()}
onToggleDir={(path) =>
setExpandedCatalogDirs((current) => {
const next = new Set(current[catalogRef] ?? []);
const next = new Set(current[selectedCatalogSkill.id] ?? []);
if (next.has(path)) next.delete(path);
else next.add(path);
return { ...current, [catalogRef]: next };
});
}}
return { ...current, [selectedCatalogSkill.id]: next };
})
}
onSelectPath={(path) => setCatalogSelectedPath(path)}
fileHref={() => `/skills?catalog=${encodeURIComponent(selectedCatalogRef)}`}
/>
)}
</aside>
<div className="min-w-0 pl-6">
<CatalogDetailPane
skill={selectedCatalogSkill}
packageName={selectedCatalogSkill?.packageName ?? (selectedCatalogSkill ? installedByKey.get(selectedCatalogSkill.key)?.packageName : null) ?? null}
packageVersion={selectedCatalogSkill?.packageVersion ?? (selectedCatalogSkill ? installedByKey.get(selectedCatalogSkill.key)?.packageVersion : null) ?? null}
installedSkill={selectedCatalogSkill ? installedByKey.get(selectedCatalogSkill.key) ?? null : null}
installedSkillId={(selectedCatalogSkill ? installedByKey.get(selectedCatalogSkill.key)?.id : null) ?? null}
fileQuery={catalogFileQuery}
selectedPath={catalogSelectedPath}
onInstall={() => selectedCatalogSkill && openInstallDialog(selectedCatalogSkill)}
onUpdate={() => selectedCatalogSkill && openInstallDialog(selectedCatalogSkill)}
onOpenInstalled={(skillId) => {
setViewParam("installed");
navigate(routeForSkillId(skillId));
}}
loadingPrimaryAction={installCatalog.isPending}
/>
</aside>
<div className="min-w-0">
<CatalogDetailPane
skill={selectedCatalogSkill}
packageName={selectedCatalogSkill.packageName ?? installedByKey.get(selectedCatalogSkill.key)?.packageName ?? null}
packageVersion={selectedCatalogSkill.packageVersion ?? installedByKey.get(selectedCatalogSkill.key)?.packageVersion ?? null}
installedSkill={installedByKey.get(selectedCatalogSkill.key) ?? null}
installedSkillId={installedByKey.get(selectedCatalogSkill.key)?.id ?? null}
fileQuery={catalogFileQuery}
selectedPath={catalogSelectedPath}
onInstall={() => openInstallDialog(selectedCatalogSkill)}
onUpdate={() => openInstallDialog(selectedCatalogSkill)}
onOpenInstalled={(skillId) => navigate(routeForSkillId(skillId))}
loadingPrimaryAction={installCatalog.isPending}
/>
</div>
</div>
</div>
)}
</div>
)}
</div>
) : (
<div className="min-h-[calc(100vh-12rem)]">
{skillsQuery.isLoading ? (
<PageSkeleton variant="detail" />
) : (
<EmptyState icon={Boxes} message="Skill not found." />
)}
</div>
)}
</>
);