diff --git a/.project-docs/30-worklog/tasks/20260905-plugin-download-action-6c8e4a21.md b/.project-docs/30-worklog/tasks/20260905-plugin-download-action-6c8e4a21.md new file mode 100644 index 0000000..3b244a0 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260905-plugin-download-action-6c8e4a21.md @@ -0,0 +1,69 @@ +# Task: Fix missing official plugin acquire and download action + +## Identity + +- Task ID: 20260905-plugin-download-action-6c8e4a21 +- Mode: Feature +- Branch: codex/20260905-plugin-download-action-6c8e4a21-plugin-download-action +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260905-plugin-download-action-6c8e4a21 +- Base commit: 9ed9bf4a715e300420e7a764a4cf576a6dfc4938 +- Owner: codex-root +- Status: Ready for Integration + +## Scope + +- Reproduce and fix the unified `/plugins` projection for the code-owned bundled + `makelore.project-scaffold` Plugin when the Account Library has not acquired it. +- Keep the existing acquisition, device-package, project-enablement, and Agent + assignment authorities separate; cover the public Renderer seam with a focused + regression test. +- Limit product changes to the unified Plugin workspace projection/presentation and + its focused tests unless the reproduction proves the fault is owned elsewhere. + +## Intent And Constraints + +- A bundled Project Scaffold Plugin never has a device download/update/uninstall + action. Before acquisition it must offer the existing free Account Library acquire + action; after acquisition it may offer project enablement and Agent assignment. +- Do not add a visible local package/Skill installation picker, change server/Main + contracts, or collapse Account Library acquisition into device installation. +- Preserve the occupied client root worktree and its three adopted untracked task + records exactly. Work only in this isolated feature worktree. +- Concurrent Task Gate: Passed. Planning Gate: Passed after loading the current + integrated state, ADR-008, Plugin business rules/module boundaries, and the completed + root Project Scaffold integration task. No active product writer overlaps this scope. + +## Outcome + +- Added one shared projection predicate for code-owned bundled official Plugins and + used it on both the unified Plugin card and detail dialog. An unacquired bundled + Project Scaffold now reports `随应用提供` rather than the false + `未下载官方包` state. +- Preserved the existing authority boundary: the client does not synthesize a + Marketplace catalog entry or acquisition action. The existing `免费获取` action + still appears only when the server catalog contains the Plugin and the authenticated + Account Library snapshot confirms it is not acquired. + +## Verification + +- TDD red: the new public Renderer regression failed because both card and detail + reported the bundled Plugin as not downloaded. +- Focused green: `plugins-page` plus `plugin-workspace-model` = 35 passed. +- `corepack pnpm run typecheck`: passed. +- Scoped ESLint on the three changed product files and focused test: passed. +- `corepack pnpm run build:vite`: passed for Renderer, Main, Preload, and utility + targets with existing size/import warnings. +- Full unit run: 225 files / 1887 passed / 2 skipped, with one unrelated real-process + timing threshold failure (`2657ms < 2000ms`). The exact failed file then passed 6/6 + in isolation; the separately skipped pressure suite passed 1/1. +- `git diff --check`: passed. + +## Follow-ups + +- Deploy the current Works Square server revision and migrate production through + `20260904_project_scaffold_0085`; until then the live catalog has no authority to + offer `免费获取` for `makelore.project-scaffold`. + +## Promotion Candidates + +- None recorded. diff --git a/src/pages/Plugins/PluginDetails.tsx b/src/pages/Plugins/PluginDetails.tsx index 9b77a2e..477d2a8 100644 --- a/src/pages/Plugins/PluginDetails.tsx +++ b/src/pages/Plugins/PluginDetails.tsx @@ -12,7 +12,11 @@ import { DialogTitle, } from '@/components/ui/dialog'; import type { DataServiceInstanceState } from '../../../shared/data-service'; -import type { PluginWorkspaceCommand, PluginWorkspaceItem } from './plugin-workspace-model'; +import { + isBundledOfficialPlugin, + type PluginWorkspaceCommand, + type PluginWorkspaceItem, +} from './plugin-workspace-model'; interface DetailOperation { capabilityId: string; @@ -286,7 +290,7 @@ export function PluginDetails(props: PluginDetailsProps) {
{props.item.source === 'local' ? (props.item.localEnabled ? '本机全局已启用' : '本机全局已停用') - : installation?.version ? `官方包 ${installation.version}` : props.item.delivery === 'system_included' ? '随应用提供' : '未下载官方包'} + : installation?.version ? `官方包 ${installation.version}` : isBundledOfficialPlugin(props.item) ? '随应用提供' : '未下载官方包'}
diff --git a/src/pages/Plugins/index.tsx b/src/pages/Plugins/index.tsx index 7ba6577..ec742fc 100644 --- a/src/pages/Plugins/index.tsx +++ b/src/pages/Plugins/index.tsx @@ -21,7 +21,7 @@ import type { PluginWorkspaceProjection, PluginWorkspaceState, } from './plugin-workspace-model'; -import { buildPluginWorkspaceProjection } from './plugin-workspace-model'; +import { buildPluginWorkspaceProjection, isBundledOfficialPlugin } from './plugin-workspace-model'; import { resolvePluginWorkspaceSearch, serializePluginWorkspaceSearch } from './plugin-workspace-query'; const DELIVERY_TEXT = { @@ -111,7 +111,7 @@ function PluginCard({

{item.summary}

当前项目
{PROJECT_TEXT[item.projectState]}
-
本机状态
{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : item.delivery === 'system_included' ? '随应用提供' : '未下载官方包'}
+
本机状态
{item.source === 'local' ? (item.localEnabled ? '本机全局已启用' : '本机全局已停用') : item.official?.installation?.version ? `官方包 ${item.official.installation.version}` : isBundledOfficialPlugin(item) ? '随应用提供' : '未下载官方包'}
伙伴
{agentText(item)}
计费
{BILLING_TEXT[item.billing]}
diff --git a/src/pages/Plugins/plugin-workspace-model.ts b/src/pages/Plugins/plugin-workspace-model.ts index d28d984..0525b45 100644 --- a/src/pages/Plugins/plugin-workspace-model.ts +++ b/src/pages/Plugins/plugin-workspace-model.ts @@ -104,6 +104,11 @@ export interface PluginWorkspaceItem { local: DevicePackageRecordV1 | null; } +export function isBundledOfficialPlugin(item: PluginWorkspaceItem): boolean { + return item.source === 'official' + && (item.delivery === 'system_included' || isCodeOwnedOptionalBundledPluginId(item.pluginId)); +} + export type PluginWorkspaceNotice = | { kind: 'scope_fallback'; message: string } | { kind: 'source_unavailable'; source: 'catalog' | 'library' | 'device' | 'project'; message: string } diff --git a/tests/unit/plugins-page.test.tsx b/tests/unit/plugins-page.test.tsx index dced718..5b31b5f 100644 --- a/tests/unit/plugins-page.test.tsx +++ b/tests/unit/plugins-page.test.tsx @@ -449,6 +449,45 @@ describe('PluginsView', () => { expect(view.onFiltersChange).toHaveBeenCalledWith(expect.objectContaining({ search: 'web' })); }); + it('labels an unacquired code-owned bundled plugin as supplied by MakeLore instead of not downloaded', () => { + const scaffoldItem: PluginWorkspaceItem = { + ...officialItem, + key: 'official:makelore.project-scaffold', + pluginId: 'makelore.project-scaffold', + title: 'MakeLore 项目脚手架', + delivery: 'not_acquired', + projectState: 'disabled', + assignedAgentIds: [], + assignedAgentNames: [], + commands: [], + official: { + catalog: null, + detail: null, + library: null, + installation: null, + project: { + ...projectPlugin, + id: 'makelore.project-scaffold', + displayName: 'MakeLore 项目脚手架', + enabled: false, + state: 'disabled', + }, + }, + }; + const view = props(scaffoldItem); + view.projection = { + items: [scaffoldItem], + selected: scaffoldItem, + counts: { all: 1, available: 0, mine: 0, enabled: 0, update: 0, unavailable: 0 }, + notices: [], + }; + + render(); + + expect(screen.getAllByText('随应用提供')).toHaveLength(2); + expect(screen.queryByText('未下载官方包')).not.toBeInTheDocument(); + }); + it('shows one dialog detail surface, confirms destructive commands, and embeds Data Service settings', () => { const view = props(officialItem); render();