diff --git a/apps/desktop/src/app/settings/plugin-install-modal.test.tsx b/apps/desktop/src/app/settings/plugin-install-modal.test.tsx index 3db67fe2f1..6b8ca1a0c5 100644 --- a/apps/desktop/src/app/settings/plugin-install-modal.test.tsx +++ b/apps/desktop/src/app/settings/plugin-install-modal.test.tsx @@ -111,4 +111,27 @@ describe('Install from Git entry flow', () => { expect(requestGateway).not.toHaveBeenCalledWith('plugins.manage', expect.objectContaining({ action: 'install' })) expect(installDesktopPlugin).not.toHaveBeenCalled() }) + + it('pins a custom install to a full commit SHA and refuses anything shorter', async () => { + probePluginRepo.mockResolvedValue({ ok: true, agent: true, desktop: false, warnings: [] }) + requestGateway.mockImplementation(async (method: string) => + method === 'plugins.manage' ? { ok: true, plugin_name: 'plugin', plugins: [] } : { plugins: [] } + ) + renderFlow() + act(() => openPluginInstallRequest({ repo: 'https://github.com/example/plugin' })) + const pin = await screen.findByRole('textbox', { name: 'Pin to commit (optional)' }) + const install = screen.getByRole('button', { name: 'Install' }) as HTMLButtonElement + fireEvent.change(pin, { target: { value: 'main' } }) + expect(install.disabled).toBe(true) + const sha = 'ABCDEF0123456789abcdef0123456789abcdef01' + fireEvent.change(pin, { target: { value: ` ${sha} ` } }) + expect(install.disabled).toBe(false) + fireEvent.click(install) + await waitFor(() => + expect(requestGateway).toHaveBeenCalledWith( + 'plugins.manage', + expect.objectContaining({ action: 'install', ref: sha.toLowerCase() }) + ) + ) + }) }) diff --git a/apps/desktop/src/app/settings/plugin-install-modal.tsx b/apps/desktop/src/app/settings/plugin-install-modal.tsx index 83fb8ee080..92cc4b8066 100644 --- a/apps/desktop/src/app/settings/plugin-install-modal.tsx +++ b/apps/desktop/src/app/settings/plugin-install-modal.tsx @@ -22,7 +22,7 @@ import { useI18n } from '@/i18n' import { ExternalLink } from '@/lib/external-link' import { AlertTriangle } from '@/lib/icons' import { resolvePluginSourceLinks } from '@/lib/plugin-source-urls' -import { installAgentPlugin, loadAgentPlugins } from '@/store/agent-plugins' +import { COMMIT_SHA_RE, installAgentPlugin, loadAgentPlugins } from '@/store/agent-plugins' import { notify } from '@/store/notifications' import { $pluginInstallRequest, @@ -57,6 +57,7 @@ export function PluginInstallModal() { const [installDesktop, setInstallDesktop] = useState(true) const [enableAgent, setEnableAgent] = useState(true) const [forceReinstall, setForceReinstall] = useState(false) + const [pinRef, setPinRef] = useState('') const [installing, setInstalling] = useState(false) const [installError, setInstallError] = useState(null) const probeToken = useRef(0) @@ -69,6 +70,7 @@ export function PluginInstallModal() { setInstallDesktop(true) setEnableAgent(true) setForceReinstall(false) + setPinRef('') setInstalling(false) setInstallError(null) }, []) @@ -195,6 +197,7 @@ export function PluginInstallModal() { force: forceReinstall, enable: enableAgent, catalogName: request.catalogName, + ref: pinRefTrimmed || undefined, profile: request.profile }) @@ -280,6 +283,8 @@ export function PluginInstallModal() { const open = request !== null && !onSettings const busy = phase === 'probing' || installing + const pinRefTrimmed = pinRef.trim().toLowerCase() + const pinRefInvalid = pinRefTrimmed !== '' && !COMMIT_SHA_RE.test(pinRefTrimmed) return ( - {[...(probe.warnings ?? []), probe.insecure ? m.insecureWarning : ''].filter(Boolean).join(' ')} + {[...new Set([...(probe.warnings ?? []), probe.insecure ? m.insecureWarning : ''])] + .filter(Boolean) + .join(' ')} )} @@ -455,6 +462,28 @@ export function PluginInstallModal() { )} + + {!request.catalogName && probe.agent && ( + + )} )} @@ -475,7 +504,10 @@ export function PluginInstallModal() { {m.reviewRepository} ) : ( - )} diff --git a/apps/desktop/src/app/skills/plugins-tab.tsx b/apps/desktop/src/app/skills/plugins-tab.tsx index 2b743f6e83..f0f119ed54 100644 --- a/apps/desktop/src/app/skills/plugins-tab.tsx +++ b/apps/desktop/src/app/skills/plugins-tab.tsx @@ -114,6 +114,13 @@ function AgentPluginListRow({ )} + {row.pinned_sha && ( + + + {t.skills.plugins.pinnedBadge(row.pinned_sha.slice(0, 8))} + + + )} {row.update_available && onUpdate && (