Marketplace UI plumbing: wrappers, singleton tab, settings section, view shell
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import type {
|
||||
ItemKind,
|
||||
MarketplaceInstall,
|
||||
MarketplaceItemRef,
|
||||
MarketplaceSnapshot,
|
||||
Project,
|
||||
} from "./types";
|
||||
|
||||
/** How a project relates to one marketplace item. */
|
||||
export type ProjectItemState =
|
||||
| "none"
|
||||
| "inherited"
|
||||
| "opted_out"
|
||||
| "project"
|
||||
| "project_pinned_differently";
|
||||
|
||||
export const KIND_ORDER: ItemKind[] = ["agent", "skill", "command", "hook", "plugin"];
|
||||
|
||||
export const KIND_LABELS: Record<ItemKind, string> = {
|
||||
agent: "Agents",
|
||||
skill: "Skills",
|
||||
command: "Commands",
|
||||
hook: "Hooks",
|
||||
plugin: "Plugins",
|
||||
};
|
||||
|
||||
/** A marketplace is refreshed when its tab opens if the last fetch is older than this. */
|
||||
export const STALE_AFTER_MS = 15 * 60 * 1000;
|
||||
|
||||
export const itemRefKey = (r: MarketplaceItemRef) => `${r.marketplace_id}/${r.kind}/${r.key}`;
|
||||
|
||||
/** Same shape as the item strings in a `SyncReport`. */
|
||||
export const formatItemRef = (r: MarketplaceItemRef) => `${r.kind}:${r.key}`;
|
||||
|
||||
const sameItem = (a: MarketplaceItemRef, b: MarketplaceItemRef) =>
|
||||
a.marketplace_id === b.marketplace_id && a.kind === b.kind && a.key === b.key;
|
||||
|
||||
export function projectItemState(
|
||||
item: MarketplaceItemRef,
|
||||
globalInstalls: MarketplaceInstall[],
|
||||
project: Project,
|
||||
): ProjectItemState {
|
||||
const own = project.marketplace_installs.find((i) => sameItem(i, item));
|
||||
const global = globalInstalls.find((i) => sameItem(i, item));
|
||||
if (own) {
|
||||
return global && global.commit !== own.commit ? "project_pinned_differently" : "project";
|
||||
}
|
||||
if (!global) return "none";
|
||||
return project.marketplace_disabled.some((d) => sameItem(d, item)) ? "opted_out" : "inherited";
|
||||
}
|
||||
|
||||
/** Mirror of the backend's `effective_installs`, tagged with where each install comes from. */
|
||||
export function effectiveInstalls(
|
||||
globalInstalls: MarketplaceInstall[],
|
||||
project: Project,
|
||||
): (MarketplaceInstall & { source: "global" | "project" })[] {
|
||||
const byKey = new Map<string, MarketplaceInstall & { source: "global" | "project" }>();
|
||||
for (const g of globalInstalls) {
|
||||
if (project.marketplace_disabled.some((d) => sameItem(d, g))) continue;
|
||||
byKey.set(itemRefKey(g), { ...g, source: "global" });
|
||||
}
|
||||
for (const p of project.marketplace_installs) {
|
||||
byKey.set(itemRefKey(p), { ...p, source: "project" });
|
||||
}
|
||||
return [...byKey.entries()]
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
.map(([, v]) => v);
|
||||
}
|
||||
|
||||
export function isStale(snapshot: MarketplaceSnapshot, now: number): boolean {
|
||||
if (!snapshot.fetched_at) return true;
|
||||
const at = Date.parse(snapshot.fetched_at);
|
||||
if (Number.isNaN(at)) return true;
|
||||
return now - at > STALE_AFTER_MS;
|
||||
}
|
||||
Reference in New Issue
Block a user