fix(desktop): surface a plugin reload failure instead of reporting success

pluginStore applies a plugin change, reloads the runtime, and resolves
either way -- a failed reload is recorded in `refreshWarning` and the
action still returns its success message. Nothing reads that field, so
"disabled the plugin but the runtime never reloaded" reached the user as
a plain success toast, indistinguishable from a clean run.

The mutation failing still throws and still surfaces, so this was not
fully silent; the half where the change lands but the reload does not
was. PluginDetail now reads `refreshWarning` after the call -- the store
clears it when a mutation starts, so anything present belongs to that
action -- and reports a warning naming the reload failure.

Kept in the component rather than the store: no store in this codebase
reaches into useUIStore, and adding the first such call to fix a toast
is not the place to introduce that.
This commit is contained in:
程序员阿江(Relakkes) 2026-07-25 22:07:12 +08:00
parent 4404adf804
commit 6900f7f9f4
7 changed files with 67 additions and 1 deletions

View File

@ -661,4 +661,52 @@ describe('Settings > Plugins tab', () => {
expect(screen.getByRole('button', { name: /codex-rescue/i })).toBeDisabled()
expect(screen.getByRole('button', { name: /gpt-5-4-prompting/i })).toBeDisabled()
})
// The store records a failed post-change runtime reload in `refreshWarning`
// and still resolves, so the action itself reads as a success. Without the
// warning surfacing, "disabled the plugin but the runtime never reloaded"
// looks identical to a clean success.
it('warns when a plugin change succeeds but the runtime reload fails', async () => {
usePluginStore.setState({
selectedPlugin: {
id: 'github@claude-plugins-official',
name: 'github',
marketplace: 'claude-plugins-official',
scope: 'user',
enabled: true,
hasErrors: false,
isBuiltin: false,
description: 'GitHub integration',
componentCounts: {
commands: 0, agents: 0, skills: 0, hooks: 0, mcpServers: 0, lspServers: 0,
},
capabilities: {
commands: [], agents: [], skills: [], hooks: [], mcpServers: [], lspServers: [],
},
commandEntries: [],
agentEntries: [],
hookEntries: [],
skillEntries: [],
mcpServerEntries: [],
errors: [],
},
disablePlugin: async () => {
usePluginStore.setState({ refreshWarning: 'runtime refresh failed' })
return 'Plugin disabled'
},
})
render(<Settings />)
switchToPluginsTab()
fireEvent.click(screen.getByRole('button', { name: 'Disable' }))
await waitFor(() => {
const toasts = useUIStore.getState().toasts
expect(
toasts.some(t => t.type === 'warning' && t.message.includes('runtime refresh failed')),
`expected a warning toast, got ${JSON.stringify(toasts)}`,
).toBe(true)
})
})
})

View File

@ -68,7 +68,20 @@ export function PluginDetail() {
setActionKey(key)
try {
const message = await fn()
addToast({ type: 'success', message })
// The store applies the change, then reloads the runtime, and resolves
// either way -- a failed reload only lands in `refreshWarning`. Reporting
// a plain success here would make "applied but never reloaded" look
// identical to a clean run. Read it after the call: the store clears it
// when the mutation starts, so anything present belongs to this action.
const refreshWarning = usePluginStore.getState().refreshWarning
if (refreshWarning) {
addToast({
type: 'warning',
message: t('settings.plugins.reloadWarning', { message: refreshWarning }),
})
} else {
addToast({ type: 'success', message })
}
} catch (err) {
addToast({
type: 'error',

View File

@ -1098,6 +1098,7 @@ export const en = {
'settings.plugins.applyHint': 'Enable, disable, and update changes become active after applying them to the current desktop runtime.',
'settings.plugins.lastReload': 'Last apply: {enabled} active plugins, {skills} plugin skills, {errors} errors.',
'settings.plugins.reloadToast': 'Applied plugin changes: {enabled} active plugins, {skills} skills, {errors} errors.',
'settings.plugins.reloadWarning': 'Plugin change applied, but reloading the runtime failed: {message}',
'settings.plugins.marketplacesTitle': 'Known marketplaces',
'settings.plugins.marketplacesHint': 'Read-only summary of marketplace sources already configured for this machine.',
'settings.plugins.marketplaceAutoUpdateOn': 'Auto-update on',

View File

@ -1100,6 +1100,7 @@ export const jp: Record<TranslationKey, string> = {
'settings.plugins.applyHint': '有効化、無効化、更新の変更は、現在のデスクトップランタイムに適用した後で有効になります。',
'settings.plugins.lastReload': '前回の適用: アクティブなプラグイン {enabled} 個、プラグインスキル {skills} 個、エラー {errors} 件。',
'settings.plugins.reloadToast': 'プラグインの変更を適用しました: アクティブなプラグイン {enabled} 個、スキル {skills} 個、エラー {errors} 件。',
'settings.plugins.reloadWarning': 'プラグインの変更は適用されましたが、ランタイムの再読み込みに失敗しました: {message}',
'settings.plugins.marketplacesTitle': '既知のマーケットプレイス',
'settings.plugins.marketplacesHint': 'このマシンに既に設定済みのマーケットプレイスソースの読み取り専用の概要です。',
'settings.plugins.marketplaceAutoUpdateOn': '自動更新オン',

View File

@ -1100,6 +1100,7 @@ export const kr: Record<TranslationKey, string> = {
'settings.plugins.applyHint': '사용, 사용 안 함, 업데이트 변경 사항은 현재 데스크톱 런타임에 적용한 후 적용됩니다.',
'settings.plugins.lastReload': '마지막 적용: 활성 플러그인 {enabled}개, 플러그인 스킬 {skills}개, 오류 {errors}건.',
'settings.plugins.reloadToast': '플러그인 변경 사항을 적용했습니다: 활성 플러그인 {enabled}개, 스킬 {skills}개, 오류 {errors}건.',
'settings.plugins.reloadWarning': '플러그인 변경 사항은 적용되었지만 런타임 다시 불러오기에 실패했습니다: {message}',
'settings.plugins.marketplacesTitle': '알려진 마켓플레이스',
'settings.plugins.marketplacesHint': '이 컴퓨터에 이미 구성된 마켓플레이스 소스의 읽기 전용 요약입니다.',
'settings.plugins.marketplaceAutoUpdateOn': '자동 업데이트 켜짐',

View File

@ -1100,6 +1100,7 @@ export const zh: Record<TranslationKey, string> = {
'settings.plugins.applyHint': '啟用、禁用或更新後,需要把外掛變更重新應用到當前桌面端執行時。',
'settings.plugins.lastReload': '最近一次應用:{enabled} 個活躍外掛,{skills} 個外掛技能,{errors} 個錯誤。',
'settings.plugins.reloadToast': '外掛變更已應用:{enabled} 個活躍外掛,{skills} 個技能,{errors} 個錯誤。',
'settings.plugins.reloadWarning': '外掛變更已應用,但執行階段重新載入失敗:{message}',
'settings.plugins.marketplacesTitle': '已知外掛市場',
'settings.plugins.marketplacesHint': '這裡只展示當前機器已經配置好的 marketplace 來源摘要。',
'settings.plugins.marketplaceAutoUpdateOn': '自動更新開',

View File

@ -1100,6 +1100,7 @@ export const zh: Record<TranslationKey, string> = {
'settings.plugins.applyHint': '启用、禁用或更新后,需要把插件变更重新应用到当前桌面端运行时。',
'settings.plugins.lastReload': '最近一次应用:{enabled} 个活跃插件,{skills} 个插件技能,{errors} 个错误。',
'settings.plugins.reloadToast': '插件变更已应用:{enabled} 个活跃插件,{skills} 个技能,{errors} 个错误。',
'settings.plugins.reloadWarning': '插件变更已应用,但运行时重载失败:{message}',
'settings.plugins.marketplacesTitle': '已知插件市场',
'settings.plugins.marketplacesHint': '这里只展示当前机器已经配置好的 marketplace 来源摘要。',
'settings.plugins.marketplaceAutoUpdateOn': '自动更新开',