diff --git a/apps/web/src/pages/agent-skills-picker.tsx b/apps/web/src/pages/agent-skills-picker.tsx index acaa774861..46699ae6a5 100644 --- a/apps/web/src/pages/agent-skills-picker.tsx +++ b/apps/web/src/pages/agent-skills-picker.tsx @@ -73,9 +73,8 @@ export function AgentSkillsPicker({ } const visibleNames = new Set(state.skills.map((skill) => skill.name)); - // A pin can outlive its skill's visibility — the author made it - // private, it was renamed, or it was discarded — leaving a name in - // `selected` with no matching registry entry. Render those as + // A pin can outlive its skill — it was renamed or discarded — leaving + // a name in `selected` with no matching registry entry. Render those as // removable rows rather than silently dropping them, so the picker // can always reach a saveable state. const staleNames = selected.filter((name) => !visibleNames.has(name)); @@ -97,8 +96,7 @@ export function AgentSkillsPicker({ {name} - No longer available — its author may have made it private, - renamed it, or discarded it. + No longer available — it may have been renamed or discarded. diff --git a/apps/web/src/pages/plugins-page.tsx b/apps/web/src/pages/plugins-page.tsx index e618ce5831..9f1deea4b8 100644 --- a/apps/web/src/pages/plugins-page.tsx +++ b/apps/web/src/pages/plugins-page.tsx @@ -39,12 +39,7 @@ import { useRequestPluginsConnect, } from "../shell/provider-health-context"; import { StageTopBar } from "../shell/stage-top-bar"; -import { - createSkill, - createSkillFromFile, - listSkills, - type SkillSummary, -} from "../skills-api"; +import { createSkill, listSkills, type SkillSummary } from "../skills-api"; import { CreateSkillDialog, type SkillCreateInput, @@ -261,14 +256,10 @@ export function PluginsRoute({ async function handleCreateSkill(input: SkillCreateInput) { if (selectedTenantId === null) return; - const skill = - input.kind === "file" - ? await createSkillFromFile(selectedTenantId, input.source) - : await createSkill(selectedTenantId, { - name: input.name, - description: input.description, - body: input.body, - }); + const skill = await createSkill(selectedTenantId, { + name: input.name, + ...(input.displayName !== "" ? { displayName: input.displayName } : {}), + }); setCreateSkillOpen(false); reloadSkills(); openSkill(skill.name); @@ -330,7 +321,6 @@ export function PluginsRoute({ assetId: skill.assetId, name: skill.name, description: skill.description, - scope: skill.scope, })); return ( diff --git a/apps/web/src/pages/skill-detail-page.tsx b/apps/web/src/pages/skill-detail-page.tsx index c5e077d692..ca203f8f34 100644 --- a/apps/web/src/pages/skill-detail-page.tsx +++ b/apps/web/src/pages/skill-detail-page.tsx @@ -1,111 +1,45 @@ -// The skill detail page at `/skills/` (CL-6416). One full page per -// skill, replacing the placeholder that stood at this route and absorbing -// the version-history panel the Skills roster used to carry inline — there -// is one skill editor now, and it lives here. +// The skill detail page at `/skills/` (CL-6416). // -// Versioning is git: every save is a commit on the skill's own SKILL.md -// (`@corbits/skills`), so "the version list" is that commit history and -// "restore" is a new commit carrying an older content. Nothing on this -// page stores a version itself. -// -// A save is never silent, and never blind: -// -// * "Review" opens a review step showing the diff between the published -// version and what is in the editor; the commit happens only on -// confirm. -// * The save carries the version the editor was seeded from, so if -// somebody else saved in the meantime the registry refuses it (409) -// and the review re-opens against what is actually published now, -// rather than burying their work. -// -// The editor buffer is newline-normalized, so the bytes reviewed in the -// diff are exactly the bytes the confirm writes. -// -// The page reads top to bottom in one column: who can see the skill, the -// description agents match on, the body, then the full version history. -// History was a narrow side column, which left every one of its columns -// truncated to a stub — a table nobody can read teaches nothing, so it now -// gets the full width it needs. - +// CL-8086: this used to be a full editor over `@corbits/skills`' own +// registry — description/body editing with diff review, restore-by- +// version off that asset's git history, a "pinned by" list, and a +// private/shared visibility toggle. That registry (and the workflow +// routes it served) was deleted: skills are native `kind:"skill"` hub +// assets now, and the stock asset routes (`@intx/hub-api`'s +// `routes/assets.ts`) carry only metadata — id, name, displayName, +// creator, timestamps. There is no stock route yet to read a skill's +// SKILL.md content, its version history, who has it pinned, or a +// scope/visibility flag, so none of that can be rendered here without +// vendoring a replacement surface. This page is scoped down to what the +// stock routes actually carry until one exists — see the CL-8086 PR body +// for the gap. import { - Badge, - Button, - Dialog, - DialogBody, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, PageShell, RichEmptyState, Section, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - Textarea, formatRelativeTime, } from "@corbits/react-ui"; -import { GitDiff, Lightning } from "@corbits/icons"; +import { Lightning } from "@corbits/icons"; import { WorkbenchLoadingState } from "@corbits/chat-ui"; import { ApiQueryError, describeApiError } from "@corbits/api-query"; -import { normalizeNewlines } from "@corbits/text-diff"; import { useCallback, useEffect, useState, type ReactNode } from "react"; import { useBench } from "../bench-context"; import { SKILLS_PATH_PREFIX, skillIdFromPath } from "../path-ids"; import { skillDisplayName } from "../skill-display-name"; -import { skillVersionSavedBy } from "../skill-version-author"; import { StageTopBar } from "../shell/stage-top-bar"; -import { - listSkillVersions, - loadSkill, - loadSkillAtVersion, - restoreSkillVersion, - setSkillScope, - updateSkill, - type PinnedByEntry, - type SkillDetail, - type SkillVersion, -} from "../skills-api"; -import { DiffHeading, DiffView } from "./diff-view"; - -type Draft = { readonly description: string; readonly body: string }; - -type Loaded = { - readonly skill: SkillDetail; - readonly pinnedBy: readonly PinnedByEntry[]; - readonly versions: readonly SkillVersion[]; -}; +import { loadSkill, type SkillSummary } from "../skills-api"; type PageState = | { readonly status: "loading" } - | ({ readonly status: "ready" } & Loaded) + | { readonly status: "ready"; readonly skill: SkillSummary } | { readonly status: "missing" } | { readonly status: "error"; readonly message: string }; -type Comparison = { readonly version: SkillVersion; readonly body: string }; - function statusOf(cause: unknown): number | undefined { return cause instanceof ApiQueryError ? cause.status : undefined; } -/** "Version 3 of 7" numbering: history is newest-first, so a row's number - * counts up from the oldest commit. */ -function versionLabel(total: number, index: number): string { - return `Version ${String(total - index)}`; -} - -function draftOf(skill: SkillDetail): Draft { - return { - description: normalizeNewlines(skill.description), - body: normalizeNewlines(skill.body), - }; -} - export function SkillDetailPage({ tenantId, name, @@ -116,59 +50,30 @@ export function SkillDetailPage({ readonly now?: number; }) { const [state, setState] = useState({ status: "loading" }); - const [draft, setDraft] = useState(null); - const [confirming, setConfirming] = useState(false); - const [saveError, setSaveError] = useState(null); - const [staleNotice, setStaleNotice] = useState(null); - const [actionError, setActionError] = useState(null); - const [busy, setBusy] = useState(false); - const [comparison, setComparison] = useState(null); - /** Reads the skill and its history. `keepDraft` protects unsaved edits: - * a background re-read must never quietly discard what someone typed. */ - const read = useCallback( - async (options: { - readonly keepDraft: boolean; - }): Promise => { - if (tenantId === null) return null; - if (!options.keepDraft) setState({ status: "loading" }); - try { - const [detail, versions] = await Promise.all([ - loadSkill(tenantId, name), - listSkillVersions(tenantId, name), - ]); - const loaded: Loaded = { - skill: detail.skill, - pinnedBy: detail.pinnedBy, - versions, - }; - setState({ status: "ready", ...loaded }); - if (!options.keepDraft) { - setDraft(draftOf(detail.skill)); - setComparison(null); - } - return loaded; - } catch (cause) { - if (options.keepDraft) { - setActionError(describeApiError(cause, "re-reading this skill")); - return null; - } - setState( - statusOf(cause) === 404 - ? { status: "missing" } - : { - status: "error", - message: describeApiError(cause, "loading this skill"), - }, - ); - return null; - } - }, - [tenantId, name], - ); + const read = useCallback(async (): Promise => { + if (tenantId === null) return; + setState({ status: "loading" }); + try { + const { skill } = await loadSkill(tenantId, name); + setState({ status: "ready", skill }); + } catch (cause) { + // report-error-ignore: the failure is the page state — missing vs + // error renders distinct honest copy, so there is nothing to report + // beyond what the user already sees. + setState( + statusOf(cause) === 404 + ? { status: "missing" } + : { + status: "error", + message: describeApiError(cause, "loading this skill"), + }, + ); + } + }, [tenantId, name]); useEffect(() => { - void read({ keepDraft: false }); + void read(); }, [read]); const crumbs = [ @@ -176,10 +81,10 @@ export function SkillDetailPage({ { label: name }, ]; - function frame(actions: ReactNode, body: ReactNode) { + function frame(body: ReactNode) { return (
- +
{body} @@ -191,7 +96,6 @@ export function SkillDetailPage({ if (tenantId === null) { return frame( - null,

Pick a workbench to see this skill.

, @@ -200,7 +104,6 @@ export function SkillDetailPage({ if (state.status === "missing") { return frame( - null, } title={`No skill named “${name}”`} @@ -212,400 +115,43 @@ export function SkillDetailPage({ if (state.status === "error") { return frame( - null, } title="Couldn't load this skill" description={state.message} - actions={[ - { label: "Retry", onClick: () => void read({ keepDraft: false }) }, - ]} + actions={[{ label: "Retry", onClick: () => void read() }]} />, ); } - if (state.status === "loading" || draft === null) { - return frame(null, ); + if (state.status === "loading") { + return frame(); } - const registryTenantId: string = tenantId; - const { skill, pinnedBy, versions } = state; - const published = draftOf(skill); - const headSha = - versions.find((version) => version.current)?.commitSha ?? null; - const shared = skill.scope === "tenant"; - const edited = - draft.body !== published.body || - draft.description !== published.description; - - /** A side action (visibility, restore, compare): its failure belongs next - * to the action, never in place of the whole page, and its success never - * throws away a dirty draft. */ - async function runSideAction(action: () => Promise) { - setActionError(null); - setBusy(true); - try { - await action(); - await read({ keepDraft: edited }); - } catch (cause) { - setActionError(describeApiError(cause, "saving that change")); - } finally { - setBusy(false); - } - } - - async function confirmSave() { - if (draft === null) return; - setSaveError(null); - setBusy(true); - try { - await updateSkill(registryTenantId, skill.name, { - description: draft.description, - body: draft.body, - expectedHeadSha: headSha, - }); - setConfirming(false); - setStaleNotice(null); - await read({ keepDraft: false }); - } catch (cause) { - if (statusOf(cause) === 409) { - // Somebody else published while this edit was open. Re-read, keep - // the typed edit, and leave the review open — now diffing against - // what is actually published. - const reread = await read({ keepDraft: true }); - setStaleNotice( - reread === null - ? "Someone else saved this skill while you were editing. Reload to see their version." - : "Someone else saved this skill while you were editing. The diff below now compares your edit with their version — confirm to save on top of it.", - ); - } else { - setSaveError(describeApiError(cause, "saving this skill")); - } - } finally { - setBusy(false); - } - } - - async function compare(version: SkillVersion) { - if (comparison?.version.commitSha === version.commitSha) { - setComparison(null); - return; - } - setActionError(null); - try { - const at = await loadSkillAtVersion( - registryTenantId, - skill.name, - version.commitSha, - ); - setComparison({ version, body: at.body }); - } catch (cause) { - setActionError(describeApiError(cause, "reading that version")); - } - } - - const saveAction = ( - - ); + const { skill } = state; return frame( - saveAction,
-
-
-

- {skillDisplayName(skill)} -

-

- Updated {formatRelativeTime(skill.updatedAtIso, now)} -

-
-
-

- {shared - ? "Everyone in this workbench can use this skill." - : "Only you can use this skill."} -

- -
-
- - {actionError === null ? null : ( -

- {actionError} +

+

+ {skillDisplayName(skill)} +

+

+ Updated {formatRelativeTime(skill.updatedAtIso, now)}

- )} - -
-
-