Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/web/src/i18n/locales/en-US/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -1668,8 +1668,6 @@
"noTagsMember": "This workspace has no reusable capability tags yet.",
"goCapabilities": "Go to Capabilities →",
"noCapabilityVersion": "No version",
"deprecatedCapabilityBadge": "Deprecated",
"deprecatedCapabilityTooltip": "This capability has been deprecated. Existing bindings keep working; once you uncheck it you cannot re-add it.",
"capabilityTypeTabs": {
"all": "All",
"mcp": "MCP",
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/i18n/locales/zh-CN/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -1668,8 +1668,6 @@
"noTagsMember": "当前工作区还没有可复用的能力标签。",
"goCapabilities": "去能力页 →",
"noCapabilityVersion": "无可用版本",
"deprecatedCapabilityBadge": "已下架",
"deprecatedCapabilityTooltip": "该能力已下架,保留旧绑定可继续使用;取消勾选后无法再选回。",
"capabilityTypeTabs": {
"all": "全部",
"mcp": "MCP",
Expand Down
70 changes: 18 additions & 52 deletions apps/web/src/pages/admin/CreateAgentDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,16 +322,13 @@ export function CreateAgentDialog({
const selectedModelID = modelID || (mode === "create" ? firstModelID : "")
const selectedModel = useMemo(() => activeModels.find((m) => m.id === selectedModelID) ?? null, [activeModels, selectedModelID])
const capabilityOptions = useMemo(() => {
// `type: ""` is a sentinel for ghost rows (deprecated bindings whose real
// type is unknown); downstream filters treat it as wildcard.
type PickerOption = {
id: string
name: string
type: CapabilityType | ""
description: string
latestVersionID: string
latestVersion: string
deprecated: boolean
section: "workspace" | "marketplace"
requiredCredentials: RequiredCredential[]
}
Expand All @@ -345,7 +342,6 @@ export function CreateAgentDialog({
description: cap.description ?? "",
latestVersionID: cap.latest_version_id ?? "",
latestVersion: cap.latest_version ?? cap.latest_published_version ?? "",
deprecated: false,
section: "workspace",
requiredCredentials: cap.required_credentials ?? [],
}))
Expand All @@ -356,43 +352,15 @@ export function CreateAgentDialog({
description: cap.description ?? "",
latestVersionID: cap.latest_version_id ?? "",
latestVersion: cap.latest_version ?? "",
deprecated: false,
section: "marketplace",
requiredCredentials: cap.required_credentials ?? [],
}))
marketplace.sort((a, b) => a.name.localeCompare(b.name))
const live: PickerOption[] = [...workspace, ...marketplace]
// Ghost bindings (edit mode): when an admin deprecates a capability the
// agent still binds, ListCapabilities hides it and the row would silently
// vanish from the picker. Merge it back as a disabled row so the user can
// deliberately unbind. The agent profile only stores names (not types),
// so type is left empty and treated as wildcard downstream.
if (mode === "edit") {
const known = new Set(live.map((c) => c.name))
for (const name of capabilities) {
if (!known.has(name)) {
live.push({
id: `ghost:${name}`,
name,
type: "",
description: "",
latestVersionID: "",
latestVersion: "",
deprecated: true,
section: "workspace",
requiredCredentials: [],
})
}
}
}
return live
}, [capabilitiesQ.data, mode, capabilities])
return [...workspace, ...marketplace]
}, [capabilitiesQ.data])
const capabilityTypeCounts = useMemo(() => {
// Ghost rows have unknown type, so they're excluded from per-type tallies
// (still count toward "all").
const counts = { all: capabilityOptions.length, mcp: 0, skill: 0 }
for (const cap of capabilityOptions) {
if (cap.deprecated) continue
if (cap.type === "mcp") counts.mcp++
else if (cap.type === "skill") counts.skill++
}
Expand All @@ -401,9 +369,7 @@ export function CreateAgentDialog({
const visibleCapabilityOptions = useMemo(
() => capabilityTypeFilter === "all"
? capabilityOptions
// Ghost rows surface under every type tab; hiding them on a non-matching
// tab would resurrect the "binding seems to have vanished" footgun.
: capabilityOptions.filter((cap) => cap.deprecated || cap.type === capabilityTypeFilter),
: capabilityOptions.filter((cap) => cap.type === capabilityTypeFilter),
[capabilityOptions, capabilityTypeFilter]
)
// Models the current engine can't drive (wrong wire protocol). Kept in the
Expand Down Expand Up @@ -779,11 +745,14 @@ export function CreateAgentDialog({
// the user a clearer error tied to the input instead of a stream error.
return
}
if (mode === "create" && allCapabilitiesQ.isLoading) return
if (!allCapabilitiesQ.isSuccess) return
const selectedCapabilities = mode === "create"
? allCapabilitiesPool.filter((cap) => selectedCapabilityIDs.includes(cap.id) && cap.latest_version_id)
: []
const capabilityNames = mode === "create" ? selectedCapabilities.map((cap) => cap.name) : capabilities
const selectableCapabilityNames = new Set(allCapabilitiesPool.map((cap) => cap.name))
const capabilityNames = mode === "create"
? selectedCapabilities.map((cap) => cap.name)
: capabilities.filter((name) => selectableCapabilityNames.has(name))
// initialCapabilities carries the per-binding pin choice. Empty
// versionID falls back to the capability's latest_version_id so the
// server's NOT NULL capability_version_id constraint is satisfied
Expand Down Expand Up @@ -960,7 +929,7 @@ export function CreateAgentDialog({
hasRequiredModel &&
(connector !== "agent_daemon" || executionMode !== "local_device" || deviceID !== "") &&
workDirValid &&
(mode !== "create" || !allCapabilitiesQ.isLoading) &&
allCapabilitiesQ.isSuccess &&
(aggregatedRequiredKinds.length === 0 || allCredentialsSatisfied)

const step1Valid =
Expand Down Expand Up @@ -1504,11 +1473,9 @@ export function CreateAgentDialog({
const index = rowCounter++
const checked = mode === "create" ? selectedCapabilityIDs.includes(cap.id) : capabilities.includes(cap.name)
const lockedNoVersion = mode === "create" && !cap.latestVersionID
const lockedDeprecatedAndUnchecked = cap.deprecated && !checked
const disabled = lockedNoVersion || lockedDeprecatedAndUnchecked
const ghostTitle = cap.deprecated ? t("agents.form.deprecatedCapabilityTooltip") : undefined
const disabled = lockedNoVersion
return (
<label key={`${sec}:${cap.id || cap.name}`} title={ghostTitle} className={"flex w-full min-w-0 items-start gap-3 px-3 py-2 text-left " + (disabled ? "cursor-not-allowed bg-surface-subtle text-fg-faint" : "cursor-pointer hover:bg-surface-subtle") + (index > 0 ? " border-t border-line-muted" : "")}>
<label key={`${sec}:${cap.id || cap.name}`} className={"flex w-full min-w-0 items-start gap-3 px-3 py-2 text-left " + (disabled ? "cursor-not-allowed bg-surface-subtle text-fg-faint" : "cursor-pointer hover:bg-surface-subtle") + (index > 0 ? " border-t border-line-muted" : "")}>
<input
type="checkbox"
className="mt-0.5 h-4 w-4 shrink-0"
Expand All @@ -1518,13 +1485,12 @@ export function CreateAgentDialog({
/>
<span className="min-w-0 flex-1">
<span className="flex min-w-0 items-center gap-2">
<span className={"min-w-0 flex-1 truncate text-sm font-medium leading-4 " + (cap.deprecated ? "text-fg-subtle" : "text-fg")}>{cap.name}</span>
{cap.type && !cap.deprecated && <span className="shrink-0"><Badge variant="neutral">{cap.type}</Badge></span>}
{sec === "marketplace" && !cap.deprecated && <span className="shrink-0"><Badge variant="neutral">{t("agents.form.capabilityBadges.marketplace")}</Badge></span>}
{cap.deprecated && <span className="shrink-0"><Badge variant="warning">{t("agents.form.deprecatedCapabilityBadge")}</Badge></span>}
{!cap.deprecated && !checked && cap.latestVersion && <span className="shrink-0"><Badge variant="primary">v{cap.latestVersion}</Badge></span>}
{!cap.deprecated && !checked && !cap.latestVersion && <span className="shrink-0"><Badge variant="warning">{t("agents.form.noCapabilityVersion")}</Badge></span>}
{!cap.deprecated && checked && cap.id && (
<span className="min-w-0 flex-1 truncate text-sm font-medium leading-4 text-fg">{cap.name}</span>
<span className="shrink-0"><Badge variant="neutral">{cap.type}</Badge></span>
{sec === "marketplace" && <span className="shrink-0"><Badge variant="neutral">{t("agents.form.capabilityBadges.marketplace")}</Badge></span>}
{!checked && cap.latestVersion && <span className="shrink-0"><Badge variant="primary">v{cap.latestVersion}</Badge></span>}
{!checked && !cap.latestVersion && <span className="shrink-0"><Badge variant="warning">{t("agents.form.noCapabilityVersion")}</Badge></span>}
{checked && cap.id && (
<CapabilityVersionPicker
capabilityID={cap.id}
fromMarketplace={sec === "marketplace"}
Expand All @@ -1536,7 +1502,7 @@ export function CreateAgentDialog({
/>
)}
</span>
{cap.description && !cap.deprecated && <span className="mt-0.5 block truncate text-sm leading-4 text-fg-subtle">{cap.description}</span>}
{cap.description && <span className="mt-0.5 block truncate text-sm leading-4 text-fg-subtle">{cap.description}</span>}
</span>
</label>
)
Expand Down