fix: close plugin workspace acceptance gaps

This commit is contained in:
2026-09-03 14:29:21 +08:00
parent 1b1f206dd8
commit c5020ae22c
10 changed files with 885 additions and 53 deletions

View File

@@ -14,6 +14,96 @@ import {
import type { DataServiceInstanceState } from '../../../shared/data-service';
import type { PluginWorkspaceCommand, PluginWorkspaceItem } from './plugin-workspace-model';
interface DetailOperation {
capabilityId: string;
operationId: string;
executionMode: 'synchronous' | 'job' | null;
enabled: boolean | null;
mutation: 'read' | 'write' | 'destructive' | null;
description: string | null;
billing: {
mode: 'included' | 'platform_metered' | 'external_account';
availability: 'available' | 'unavailable' | null;
status: 'billing_unavailable' | null;
notice: string;
entitlementScope: string | null;
unitName: string | null;
unitSize: number | null;
ratePoints: string | null;
minimumChargePoints: string | null;
roundingMode: 'ceil' | null;
pricingVersion: string | number | null;
};
}
function detailOperations(item: PluginWorkspaceItem): DetailOperation[] {
const operations = new Map<string, DetailOperation>();
for (const operation of item.official?.detail?.operations ?? []) {
operations.set(`${operation.capabilityId}:${operation.operation}`, {
capabilityId: operation.capabilityId,
operationId: operation.operation,
executionMode: operation.executionMode,
enabled: operation.enabled,
mutation: null,
description: null,
billing: {
...operation.billing,
availability: null,
},
});
}
for (const capability of item.official?.project?.capabilities ?? []) {
for (const operation of capability.operations) {
const key = `${capability.id}:${operation.id}`;
const catalogOperation = operations.get(key);
operations.set(key, {
capabilityId: capability.id,
operationId: operation.id,
executionMode: catalogOperation?.executionMode ?? null,
enabled: catalogOperation?.enabled ?? null,
mutation: operation.tool?.mutation ?? null,
description: operation.tool?.description ?? null,
billing: {
mode: operation.billing.mode,
availability: operation.billing.availability,
status: null,
notice: operation.billing.notice,
entitlementScope: null,
unitName: operation.billing.unitName ?? null,
unitSize: operation.billing.unitSize ?? null,
ratePoints: operation.billing.ratePoints ?? null,
minimumChargePoints: operation.billing.minimumChargePoints ?? null,
roundingMode: operation.billing.roundingMode ?? null,
pricingVersion: operation.billing.pricingVersion ?? null,
},
});
}
}
return [...operations.values()];
}
function operationGroups(operations: readonly DetailOperation[]): Array<{
capabilityId: string;
operations: DetailOperation[];
}> {
const groups = new Map<string, DetailOperation[]>();
for (const operation of operations) {
const group = groups.get(operation.capabilityId) ?? [];
group.push(operation);
groups.set(operation.capabilityId, group);
}
return [...groups].map(([capabilityId, groupOperations]) => ({
capabilityId,
operations: groupOperations,
}));
}
function operationBillingMode(mode: DetailOperation['billing']['mode']): string {
if (mode === 'included') return '包含';
if (mode === 'platform_metered') return '按 Token Point';
return '外部账号';
}
const SOURCE_TEXT = {
official: '官方',
local: '本机',
@@ -60,6 +150,7 @@ function commandLabel(command: PluginWorkspaceCommand, title: string): string {
case 'enable_local': return `本机全局启用${title}`;
case 'disable_local': return `本机全局停用${title}`;
case 'remove_local': return `移除本机包${title}`;
case 'sign_in': return `登录后免费获取${title}`;
case 'open_agent_assignment': return `分配${title}给伙伴`;
case 'open_settings': return `查看${title}插件设置`;
}
@@ -126,6 +217,7 @@ export function PluginDetails(props: PluginDetailsProps) {
const project = props.item.official?.project ?? null;
const detail = props.item.official?.detail ?? null;
const installation = props.item.official?.installation ?? null;
const operations = detailOperations(props.item);
const showDataService = project?.enabled
&& project.settingsSurface === 'data-service';
const confirmCopy = confirmation
@@ -149,7 +241,10 @@ export function PluginDetails(props: PluginDetailsProps) {
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{SOURCE_TEXT[props.item.source]}</Badge>
{props.item.stale ? <Badge variant="warning">缓存</Badge> : null}
{props.item.unavailable ? <Badge variant="warning">暂不可用</Badge> : null}
{props.item.suspended ? <Badge variant="warning">已暂停</Badge> : null}
{props.item.retired ? <Badge variant="warning">已退役</Badge> : null}
{props.item.unavailable && !props.item.suspended && !props.item.retired
? <Badge variant="warning">暂不可用</Badge> : null}
</div>
<DialogTitle className="text-balance text-2xl">{props.item.title}</DialogTitle>
<DialogDescription>
@@ -190,6 +285,11 @@ export function PluginDetails(props: PluginDetailsProps) {
<dd className="mt-1 text-sm font-semibold">{PROJECT_TEXT[props.item.projectState]}</dd>
</div>
</dl>
{props.item.deviceReason ? (
<p className="mt-3 rounded-xl bg-warning/10 p-3 text-pretty text-sm text-warning">
{installation?.version ? '旧设备版本已保留' : '设备包不可用'}:{props.item.deviceReason}
</p>
) : null}
</section>
<section aria-labelledby="plugin-actions-heading">
@@ -217,20 +317,38 @@ export function PluginDetails(props: PluginDetailsProps) {
<section aria-labelledby="plugin-capabilities-heading">
<h3 id="plugin-capabilities-heading" className="text-lg font-semibold">能力与权限</h3>
{project?.capabilities.length ? (
{operations.length ? (
<div className="mt-3 space-y-3">
{project.capabilities.map((capability) => (
<article key={capability.id} className="rounded-xl border border-border/70 p-4">
<h4 className="font-mono text-sm font-semibold">{capability.id}</h4>
{operationGroups(operations).map((capability) => (
<article key={capability.capabilityId} className="rounded-xl border border-border/70 p-4">
<h4 className="font-mono text-sm font-semibold">{capability.capabilityId}</h4>
<ul className="mt-3 space-y-3">
{capability.operations.map((operation) => (
<li key={operation.id} className="rounded-lg bg-surface-subtle p-3 text-sm">
<li key={operation.operationId} className="rounded-lg bg-surface-subtle p-3 text-sm">
<div className="flex flex-wrap items-center gap-2">
<code className="font-semibold">{operation.id}</code>
{operation.tool ? <Badge variant="outline">{operation.tool.mutation}</Badge> : <Badge variant="outline">SDK</Badge>}
<code className="font-semibold">{operation.operationId}</code>
{operation.mutation
? <Badge variant="outline">{operation.mutation}</Badge>
: operation.executionMode
? <Badge variant="outline">{operation.executionMode === 'job' ? '异步任务' : '同步'}</Badge>
: <Badge variant="outline">SDK</Badge>}
{operation.enabled === false ? <Badge variant="warning">服务端已停用</Badge> : null}
{operation.billing.availability === 'unavailable'
|| operation.billing.status === 'billing_unavailable'
? <Badge variant="warning">计费暂不可用</Badge> : null}
</div>
{operation.tool ? <p className="mt-2 text-pretty text-muted-foreground">{operation.tool.description}</p> : null}
{operation.description ? <p className="mt-2 text-pretty text-muted-foreground">{operation.description}</p> : null}
<p className="mt-2 text-xs">{operation.billing.notice}</p>
<dl className="mt-2 grid gap-x-4 gap-y-1 text-xs text-muted-foreground sm:grid-cols-2">
<div><dt className="inline">计费方式:</dt><dd className="inline">{operationBillingMode(operation.billing.mode)}</dd></div>
{operation.billing.entitlementScope ? <div><dt className="inline">权益范围:</dt><dd className="inline">{operation.billing.entitlementScope}</dd></div> : null}
{operation.billing.unitName ? <div><dt className="inline">计费单位:</dt><dd className="inline">{operation.billing.unitName}</dd></div> : null}
{operation.billing.unitSize !== null ? <div><dt className="inline">单位大小:</dt><dd className="inline">{operation.billing.unitSize}</dd></div> : null}
{operation.billing.ratePoints !== null ? <div><dt className="inline">每单位 Token Point:</dt><dd className="inline">{operation.billing.ratePoints}</dd></div> : null}
{operation.billing.minimumChargePoints !== null ? <div><dt className="inline">最低扣点:</dt><dd className="inline">{operation.billing.minimumChargePoints}</dd></div> : null}
{operation.billing.pricingVersion !== null ? <div><dt className="inline">价格版本:</dt><dd className="inline">{operation.billing.pricingVersion}</dd></div> : null}
{operation.billing.roundingMode === 'ceil' ? <div><dt className="inline">舍入方式:</dt><dd className="inline">向上取整</dd></div> : null}
</dl>
</li>
))}
</ul>

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, type FormEvent } from 'react';
import { AlertCircle, Puzzle, RefreshCw, Search } from 'lucide-react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
@@ -22,7 +22,7 @@ import type {
PluginWorkspaceState,
} from './plugin-workspace-model';
import { buildPluginWorkspaceProjection } from './plugin-workspace-model';
import { parsePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query';
const DELIVERY_TEXT = {
system_included: '随 MakeLore 提供',
@@ -102,7 +102,9 @@ function PluginCard({
<Badge variant={item.source === 'local' ? 'secondary' : 'outline'}>{cardSource(item)}</Badge>
<Badge variant="outline">{DELIVERY_TEXT[item.delivery]}</Badge>
{item.updateAvailable ? <Badge variant="success">可更新</Badge> : null}
{item.unavailable ? <Badge variant="warning">不可用</Badge> : null}
{item.suspended ? <Badge variant="warning">已暂停</Badge> : null}
{item.retired ? <Badge variant="warning">已退役</Badge> : null}
{item.unavailable && !item.suspended && !item.retired ? <Badge variant="warning">不可用</Badge> : null}
{item.stale ? <Badge variant="warning">缓存</Badge> : null}
</div>
<Title className="mt-4 text-balance text-xl font-semibold">{item.title}</Title>
@@ -113,6 +115,11 @@ function PluginCard({
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">伙伴</dt><dd className="text-right font-medium">{agentText(item)}</dd></div>
<div className="flex justify-between gap-4"><dt className="text-muted-foreground">计费</dt><dd className="text-right font-medium">{BILLING_TEXT[item.billing]}</dd></div>
</dl>
{item.deviceReason ? (
<p className="mt-3 text-pretty text-xs leading-5 text-warning">
{item.official?.installation?.version ? '旧设备版本已保留' : '设备包不可用'}:{item.deviceReason}
</p>
) : null}
<Button
type="button"
variant="outline"
@@ -338,10 +345,16 @@ function isSettled(state: 'idle' | 'loading' | 'ready' | 'error'): boolean {
return state === 'ready' || state === 'error';
}
function scopeFallbackSearchFromLocationState(state: unknown): string | null {
if (!state || typeof state !== 'object') return null;
const value = (state as Record<string, unknown>).pluginWorkspaceScopeFallback;
return typeof value === 'string' ? value : null;
}
export function Plugins() {
const navigate = useNavigate();
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
const canonicalized = useRef(false);
const user = useAuthStore((state) => state.user);
const accountKey = user?.userId ?? user?.username ?? null;
const activeProject = useCodingWorkspaceStore((state) => state.activeProject);
@@ -372,10 +385,15 @@ export function Plugins() {
const workspaceHydrated = workspaceLoadState === 'ready' || workspaceLoadState === 'error';
const queryString = searchParams.toString();
const filters = useMemo(
() => parsePluginWorkspaceSearch(new URLSearchParams(queryString), Boolean(activeProject)),
const searchResolution = useMemo(
() => resolvePluginWorkspaceSearch(new URLSearchParams(queryString), Boolean(activeProject)),
[activeProject, queryString],
);
const fallbackLocationSearch = scopeFallbackSearchFromLocationState(location.state);
const scopeFallbackNotice = searchResolution.scopeFallback || (
!activeProject && fallbackLocationSearch === queryString
);
const filters = searchResolution.filters;
const currentProjectProjection = activeProject
&& projectProjection?.project.localProjectId === activeProject.id
? projectProjection
@@ -384,7 +402,7 @@ export function Plugins() {
() => Object.fromEntries((workspaceConfig?.agents ?? []).map((agent) => [agent.id, agent.name || agent.roleName])),
[workspaceConfig],
);
const projection = useMemo(() => buildPluginWorkspaceProjection({
const baseProjection = useMemo(() => buildPluginWorkspaceProjection({
catalog,
details,
library,
@@ -393,25 +411,65 @@ export function Plugins() {
project: currentProjectProjection,
activeProject: activeProject ? { id: activeProject.id, name: activeProject.name } : null,
agentNames,
authenticated: Boolean(accountKey),
sourceFailures: {
catalog: catalogState === 'error',
library: Boolean(accountKey) && libraryState === 'error',
},
filters,
}), [
activeProject,
agentNames,
catalog,
catalogState,
currentProjectProjection,
details,
devicePackages,
filters,
installations,
library,
libraryState,
accountKey,
]);
const projection = useMemo<PluginWorkspaceProjection>(() => {
if (!scopeFallbackNotice && !searchResolution.scopeFallback) return baseProjection;
return {
...baseProjection,
notices: [
{ kind: 'scope_fallback', message: '当前没有打开的项目,已显示全部插件。' },
...baseProjection.notices.filter((notice) => notice.kind !== 'scope_fallback'),
],
};
}, [baseProjection, scopeFallbackNotice, searchResolution.scopeFallback]);
useEffect(() => {
if (!workspaceHydrated || canonicalized.current) return;
canonicalized.current = true;
const canonical = serializePluginWorkspaceSearch(filters);
if (canonical.toString() !== queryString) setSearchParams(canonical, { replace: true });
}, [filters, queryString, setSearchParams, workspaceHydrated]);
if (!workspaceHydrated) return;
const nextFallbackSearch = searchResolution.scopeFallback
? searchResolution.canonicalSearch
: null;
const mustClearFallbackState = fallbackLocationSearch !== null
&& (Boolean(activeProject) || queryString !== fallbackLocationSearch);
if (
searchResolution.canonicalSearch !== queryString
|| fallbackLocationSearch !== nextFallbackSearch && (
searchResolution.scopeFallback || mustClearFallbackState
)
) {
setSearchParams(new URLSearchParams(searchResolution.canonicalSearch), {
replace: true,
state: nextFallbackSearch
? { pluginWorkspaceScopeFallback: nextFallbackSearch }
: null,
});
}
}, [
activeProject,
fallbackLocationSearch,
queryString,
searchResolution,
setSearchParams,
workspaceHydrated,
]);
useEffect(() => {
if (!workspaceHydrated) return;
@@ -500,7 +558,7 @@ export function Plugins() {
}, [accountKey, activeProject, catalogState, detailState, deviceState, libraryState, projectLoadState, selectedOfficialId]);
const setFilters = (next: PluginWorkspaceFilters) => {
setSearchParams(serializePluginWorkspaceSearch(next));
setSearchParams(serializePluginWorkspaceSearch(next), { state: null });
};
const select = (key: PluginWorkspaceItem['key'] | null) => {
setFilters({ ...filters, selectedKey: key });
@@ -526,6 +584,7 @@ export function Plugins() {
case 'enable_local':
case 'disable_local': return Boolean(devicePending[`enabled:${command.packageId}`]);
case 'remove_local': return Boolean(devicePending[`uninstall:${command.packageId}`]);
case 'sign_in':
case 'open_agent_assignment':
case 'open_settings': return false;
}

View File

@@ -53,6 +53,9 @@ export async function dispatchPluginWorkspaceCommand(
case 'remove_local':
await dependencies.device.uninstall(command.packageId);
return;
case 'sign_in':
dependencies.navigate('/login');
return;
case 'open_agent_assignment':
dependencies.navigate('/project-config');
return;

View File

@@ -47,6 +47,7 @@ export type PluginWorkspaceCommand =
| { kind: 'enable_local'; packageId: string }
| { kind: 'disable_local'; packageId: string }
| { kind: 'remove_local'; packageId: string }
| { kind: 'sign_in' }
| { kind: 'open_agent_assignment'; projectId: string; pluginId: string }
| { kind: 'open_settings'; projectId: string; pluginId: string };
@@ -94,6 +95,9 @@ export interface PluginWorkspaceItem {
updateAvailable: boolean;
unavailable: boolean;
stale: boolean;
suspended: boolean;
retired: boolean;
deviceReason: string | null;
commands: readonly PluginWorkspaceCommand[];
official: PluginWorkspaceOfficialSources | null;
local: DevicePackageRecordV1 | null;
@@ -113,6 +117,8 @@ export interface PluginWorkspaceInputs {
project: CodingPluginProject | null;
activeProject: { id: string; name: string } | null;
agentNames: Readonly<Record<string, string>>;
authenticated: boolean;
sourceFailures?: Readonly<{ catalog?: boolean; library?: boolean }>;
filters: PluginWorkspaceFilters;
}
@@ -157,6 +163,14 @@ function currentLibraryEntry(entry: MarketplaceLibraryEntry | null): boolean {
}
function installed(installation: MarketplaceInstallation | null): boolean {
return installation?.status === 'installed'
|| installation?.status === 'kept'
|| (installation?.status === 'unavailable'
&& Boolean(installation.version)
&& Boolean(installation.channel));
}
function installationReady(installation: MarketplaceInstallation | null): boolean {
return installation?.status === 'installed' || installation?.status === 'kept';
}
@@ -182,8 +196,10 @@ function officialCommands(
input: PluginWorkspaceInputs,
): PluginWorkspaceCommand[] {
const commands: PluginWorkspaceCommand[] = [];
const libraryCurrent = currentLibraryEntry(record.library);
const removed = record.library?.removedAt !== null && record.library?.removedAt !== undefined;
const libraryCurrent = input.authenticated && currentLibraryEntry(record.library);
const removed = input.authenticated
&& record.library?.removedAt !== null
&& record.library?.removedAt !== undefined;
const systemIncluded = record.library?.acquisitionMode === 'system_included'
|| record.catalog?.acquisition === 'system_included';
const bundledDelivery = systemIncluded || isCodeOwnedOptionalBundledPluginId(record.pluginId);
@@ -193,9 +209,12 @@ function officialCommands(
const hasDevicePackage = !bundledDelivery && installed(record.installation);
const deliveryReady = systemIncluded
|| (bundledDelivery && libraryCurrent)
|| (libraryCurrent && hasDevicePackage);
|| (libraryCurrent && !bundledDelivery && installationReady(record.installation));
if (!systemIncluded && libraryCurrent) {
if (!systemIncluded && !input.authenticated
&& record.catalog?.acquisition === 'free' && sourceAvailable) {
commands.push({ kind: 'sign_in' });
} else if (!systemIncluded && libraryCurrent) {
commands.push({ kind: 'remove_from_library', pluginId: record.pluginId });
} else if (!systemIncluded && removed && sourceAvailable) {
commands.push({ kind: 'reacquire', pluginId: record.pluginId });
@@ -246,6 +265,9 @@ function officialItem(
const systemIncluded = record.library?.acquisitionMode === 'system_included'
|| record.catalog?.acquisition === 'system_included';
const current = currentLibraryEntry(record.library);
const suspended = record.catalog?.runtimeStatus === 'suspended'
|| record.library?.runtimeStatus === 'suspended';
const retired = record.library?.catalogStatus === 'retired';
const projectState: PluginWorkspaceProjectState = !input.activeProject
? 'not_applicable'
: !record.project
@@ -266,23 +288,31 @@ function officialItem(
version: record.installation?.version ?? record.project?.version ?? base?.stableVersion ?? null,
delivery: systemIncluded
? 'system_included'
: current
: input.authenticated && current
? 'account_acquired'
: record.library?.removedAt
: input.authenticated && record.library?.removedAt
? 'account_removed'
: input.library ? 'not_acquired' : 'account_unknown',
: input.authenticated && input.library ? 'not_acquired' : 'account_unknown',
projectState,
localEnabled: null,
assignedAgentIds: agents.ids,
assignedAgentNames: agents.names,
billing: record.detail?.usageBilling ?? record.catalog?.usageBilling ?? 'unknown',
updateAvailable: updateAvailable(record),
unavailable: record.catalog?.runtimeStatus === 'suspended'
|| record.library?.runtimeStatus === 'suspended'
|| record.library?.catalogStatus === 'retired'
unavailable: suspended
|| retired
|| record.installation?.status === 'unavailable'
|| record.project?.state === 'unavailable',
stale: Boolean(record.detail?.stale || input.catalog?.stale || input.library?.stale),
stale: Boolean(
record.detail?.stale
|| input.catalog?.stale
|| input.library?.stale
|| (record.catalog && input.sourceFailures?.catalog)
|| (record.library && input.sourceFailures?.library)
),
suspended,
retired,
deviceReason: record.installation?.reason ?? null,
commands: officialCommands(record, input),
official: {
catalog: record.catalog,
@@ -316,6 +346,9 @@ function localItem(local: DevicePackageRecordV1): PluginWorkspaceItem {
updateAvailable: false,
unavailable: false,
stale: false,
suspended: false,
retired: false,
deviceReason: null,
commands: [
{ kind: local.enabled ? 'disable_local' : 'enable_local', packageId: local.packageId },
{ kind: 'remove_local', packageId: local.packageId },
@@ -327,32 +360,53 @@ function localItem(local: DevicePackageRecordV1): PluginWorkspaceItem {
function retainedItem(
pluginId: string,
activeProject: PluginWorkspaceInputs['activeProject'],
projectId: string | null,
record: OfficialAccumulator | null,
input: PluginWorkspaceInputs,
): PluginWorkspaceItem {
const base = record?.detail ?? record?.catalog ?? record?.library;
const suspended = record?.catalog?.runtimeStatus === 'suspended'
|| record?.library?.runtimeStatus === 'suspended';
const retired = record?.library?.catalogStatus === 'retired';
return {
key: `retained:${pluginId}`,
source: 'retained',
pluginId,
packageId: null,
title: pluginId,
summary: '项目配置仍保留此插件 ID,但当前来源无法解释它。',
category: null,
tags: [],
publisher: null,
version: null,
title: base?.title ?? pluginId,
summary: base?.summary ?? '项目配置仍保留此插件 ID,但当前项目投影无法解释它。',
category: base?.category ?? null,
tags: record?.detail?.tags ?? record?.catalog?.tags ?? [],
publisher: record?.detail?.providerDisplayName ?? record?.catalog?.providerDisplayName ?? null,
version: record?.installation?.version ?? base?.stableVersion ?? null,
delivery: 'retained',
projectState: 'unavailable',
localEnabled: null,
assignedAgentIds: [],
assignedAgentNames: [],
billing: 'unknown',
updateAvailable: false,
billing: record?.detail?.usageBilling ?? record?.catalog?.usageBilling ?? 'unknown',
updateAvailable: record ? updateAvailable(record) : false,
unavailable: true,
stale: false,
commands: activeProject
? [{ kind: 'disable_project', projectId: activeProject.id, pluginId }]
stale: Boolean(
record?.detail?.stale
|| (record?.catalog && input.catalog?.stale)
|| (record?.library && input.library?.stale)
|| (record?.catalog && input.sourceFailures?.catalog)
|| (record?.library && input.sourceFailures?.library)
),
suspended,
retired,
deviceReason: record?.installation?.reason ?? null,
commands: projectId
? [{ kind: 'disable_project', projectId, pluginId }]
: [],
official: null,
official: record ? {
catalog: record.catalog,
detail: record.detail,
library: record.library,
installation: record.installation,
project: record.project,
} : null,
local: null,
};
}
@@ -428,8 +482,13 @@ export function buildPluginWorkspaceProjection(input: PluginWorkspaceInputs): Pl
.sort((left, right) => left.displayName.localeCompare(right.displayName))
.map(localItem);
const retainedItems = orderedUnique(input.project?.unknownPluginIds ?? [])
.filter((pluginId) => pluginId !== NATIVE_WEB_SEARCH_ID && !records.has(pluginId))
.map((pluginId) => retainedItem(pluginId, input.activeProject));
.filter((pluginId) => pluginId !== NATIVE_WEB_SEARCH_ID && !records.get(pluginId)?.project)
.map((pluginId) => retainedItem(
pluginId,
input.project?.project.localProjectId ?? null,
records.get(pluginId) ?? null,
input,
));
const scopedItems = [...officialItems, ...localItems, ...retainedItems];
const sourceAndSearchItems = scopedItems.filter((item) => {
const sourceMatches = input.filters.source === 'all' || item.source === input.filters.source;

View File

@@ -52,3 +52,21 @@ export function serializePluginWorkspaceSearch(filters: PluginWorkspaceFilters):
if (filters.selectedKey) search.set('plugin', filters.selectedKey);
return search;
}
export interface PluginWorkspaceSearchResolution {
filters: PluginWorkspaceFilters;
canonicalSearch: string;
scopeFallback: boolean;
}
export function resolvePluginWorkspaceSearch(
search: URLSearchParams,
hasActiveProject: boolean,
): PluginWorkspaceSearchResolution {
const filters = parsePluginWorkspaceSearch(search, hasActiveProject);
return {
filters,
canonicalSearch: serializePluginWorkspaceSearch(filters).toString(),
scopeFallback: search.get('scope') === 'project' && !hasActiveProject,
};
}