From f8912d267f439ebaf0fb05dbeea430174802d2a5 Mon Sep 17 00:00:00 2001 From: creatixchu Date: Tue, 20 Oct 2026 15:46:43 +0800 Subject: [PATCH] fix(plugins): retain independent row controls during query suspension --- .../ui-plugin-manager/src/client/manager-store.ts | 14 ++++++++++---- .../experimental/cua/tests/panel.client.spec.tsx | 11 +++++------ scripts/gen-cordis-catalog.ts | 1 + 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/client/ui-plugin-manager/src/client/manager-store.ts b/packages/client/ui-plugin-manager/src/client/manager-store.ts index b036944ebe..11c04fb2c1 100644 --- a/packages/client/ui-plugin-manager/src/client/manager-store.ts +++ b/packages/client/ui-plugin-manager/src/client/manager-store.ts @@ -45,7 +45,7 @@ import { BundleDetailController, type BundleDetailsState } from './detail-store. import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client' /** - * Read whether a bundle or one of its rows has a pending management action. + * Read whether a bundle has a pending management action. * @param state Current manager snapshot. * @param packageName Bundle whose controls and observers must wait. * @returns Whether the bundle's management operation is still pending. @@ -53,8 +53,7 @@ import type { LocaleSnapshot } from '@deepseek-ai/dsh-client-locale/client' export function bundleBusy(state: PluginManagerState, packageName: string): boolean { return state.busy.includes(packageName) || state.update?.packageName === packageName || (state.install.subject?.name === packageName && (isInstallPending(state.install.phase) || state.install.phase === 'checking')) - || (state.packages.find(pkg => pkg.name === packageName)?.rows.some(row => - row.entryId !== undefined && state.busy.includes(rowKey(row.entryId))) ?? false) + } /** The action a failed notice names. */ @@ -690,7 +689,14 @@ export class PluginManagerController { * @returns A read-only source; unsubscribe releases the caller's observation. */ observeChanges(packageName: string): HostObservable { - return { getSnapshot: () => bundleBusy(this.getSnapshot(), packageName), subscribe: listener => this.store.subscribe(listener) } + return { + getSnapshot: () => { + const state = this.getSnapshot() + return bundleBusy(state, packageName) || (state.packages.find(pkg => pkg.name === packageName)?.rows.some(row => + row.entryId !== undefined && state.busy.includes(rowKey(row.entryId))) ?? false) + }, + subscribe: listener => this.store.subscribe(listener), + } } /** Stop publishing and drop every late settlement. */ diff --git a/packages/experimental/cua/tests/panel.client.spec.tsx b/packages/experimental/cua/tests/panel.client.spec.tsx index 90b49ca44d..387038390c 100644 --- a/packages/experimental/cua/tests/panel.client.spec.tsx +++ b/packages/experimental/cua/tests/panel.client.spec.tsx @@ -134,7 +134,7 @@ it('shows query failures as unknown while retaining previously displayed apps', fireEvent.click(screen.getByRole('button', { name: en.retry })) expect(f.props.refresh).toHaveBeenCalledTimes(2) f.publish({ system: { ...f.state.getSnapshot().system, busy: true }, busy: true }) - expect(screen.queryByRole('button', { name: en.retry })).toBeNull() + expect(screen.getByRole('button', { name: en.retry }).hasAttribute('disabled')).toBe(true) f.publish({ system: { ...f.state.getSnapshot().system, busy: false, error: null }, busy: false }) expect(screen.queryByRole('button', { name: en.retry })).toBeNull() f.publish(observe(value)) @@ -292,7 +292,7 @@ it('keeps content mounted and disables actions while management changes are pend const requirements = screen.getByRole('region', { name: en.title }) expect(within(requirements).getAllByRole('group')).toHaveLength(4) expect(screen.getByRole('alert').textContent).toContain('provider is reloading') - expect(screen.getByRole('button', { name: en.unknown }).hasAttribute('disabled')).toBe(true) + expect(within(screen.getByRole('group', { name: en.accessibility })).getByRole('button', { name: en.unknown }).hasAttribute('disabled')).toBe(true) fireEvent.focus(window) expect(f.props.refresh).toHaveBeenCalledTimes(2) f.publish({ system: { ...f.state.getSnapshot().system, busy: true, error: null }, busy: true }) @@ -388,14 +388,13 @@ it('loads the runtime setting independently and reports unavailable and read-onl }) -it('retains the previous single control while a stale query error is being refreshed', () => { +it('retains uncertainty and the diagnostic until a refresh succeeds', () => { const f = fixture() f.publish(observe(value)) render() f.publish({ system: { value: value.system, busy: true, error: 'stale error' }, busy: true }) - expect(screen.getByRole('button', { name: en.request }).hasAttribute('disabled')).toBe(true) - expect(screen.getByRole('button', { name: en.granted }).hasAttribute('disabled')).toBe(true) - expect(screen.queryByRole('alert')).toBeNull() + for (const button of screen.getAllByRole('button', { name: en.unknown })) expect(button.hasAttribute('disabled')).toBe(true) + expect(screen.getByRole('alert').textContent).toContain('stale error') }) diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 9398831552..cc22a870be 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -197,6 +197,7 @@ export const SERVICE_WALK_EXEMPTIONS: Record = { conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API', layout: 'client-side interface-typed browser service — packages/client/ui-layout/README.md owns the API', pluginNavigation: 'client-side bundle navigation — packages/client/ui-plugin-manager/README.md owns the API', + pluginChanges: 'client-side read-only management activity — packages/client/ui-plugin-manager/README.md owns the API', locale: 'client-side interface-typed browser service — packages/client/locale/README.md owns the API', modelDirectories: 'client-side interface-typed browser service — packages/client/ui-model-selection/README.md owns the API', modules: 'client-side interface-typed browser service — packages/client/modules/README.md owns the API',