()
- const capabilities = rows.flatMap((row) => {
- const metadataTitle = row.meta?.title === undefined ? undefined : resolveText(row.meta.title).trim()
- const title = metadataTitle === row.moduleName || metadataTitle === row.rowId ? undefined : metadataTitle
- const description = row.meta?.description === undefined ? undefined : resolveText(row.meta.description).trim() || undefined
- if (title === undefined && description === undefined) return []
- const identity = JSON.stringify([title, description])
- if (seen.has(identity)) return []
- seen.add(identity)
- return [{ rowId: row.rowId, title, description }]
- })
- if (capabilities.length === 0) return null
- return (
-
- {t('capabilitiesTitle')}
-
- {capabilities.map(({ rowId, title, description }) => (
-
- {title === undefined ? null : {title} }
- {description === undefined || description === title ? null : {description} }
-
- ))}
-
-
- )
-}
-
/**
* One package's page: the crumb back to the list; its icon with its switch
* and, for a package the profile installed, uninstall; its title beside its
- * version, beta, and problem tags; usage guidance, readable capabilities,
- * configuration, and component diagnostics folded until requested or failed.
+ * version tag, its beta tag, and its problem tag; the package name the title
+ * stands for, which is what installs it elsewhere; its one-liner; the Host's
+ * problem when it reports one; the configuration the bundle registered for
+ * itself; and its rows with their switches and configure controls.
*/
function PackageDetail({
pkg, t, resolveText, busy, rowBusy, configured, configure, renderSlot,
@@ -631,11 +601,6 @@ function PackageDetail({
const { title, description, beta } = packageText(pkg, resolveText)
const status = packageStatus(pkg)
const subject: PluginsSubject = { kind: 'bundle', pkg: packageRef(pkg) }
- const hasProblem = pkg.error !== undefined || pkg.meta?.error !== undefined
- || pkg.rows.some(row => row.phase === 'failed' || row.meta?.error !== undefined)
- const [componentsOpen, setComponentsOpen] = useState(hasProblem)
- const componentsId = useId()
- useEffect(() => { if (hasProblem) setComponentsOpen(true) }, [hasProblem])
return (
{t('statusProblem')} : null}
{renderSlot('plugins.detail.badge', { subject })}
+ {pkg.name}
{description === undefined ? null : {description}
}
-
- {t('deploymentScope')}
-
-
-
-
-
-
{pkg.error === undefined ? null : {t('reasonLabel')}: {managementText(pkg.error, t)}
}
{pkg.readOnlyReason === undefined ? null : {managementText({ code: pkg.readOnlyReason }, t)}
}
- {renderSlot('plugins.bundle.usage', { pkg: subject.pkg }, { entryKey: pkg.name })}
-
- {pkg.overrides.length === 0 ? null : (
-
- {t('overridesTitle')}
- {t('overridesDescription', { count: String(pkg.overrides.length) })}
-
- )}
{configured
? (
@@ -702,32 +652,13 @@ function PackageDetail({
)
: null}
-
- { setComponentsOpen(value => !value) }}>
-
- {t('componentsDisclosure')}
- {partsSummary(pkg.rows, t)}
-
-
- {componentsOpen ? <>
-
{pkg.name}
-
busy || rowBusy(row), onSetEnabled: onSetRowEnabled } : undefined}
- configure={configure}
- />
- {pkg.overrides.length === 0 ? null : (
-
- {t('overridesComponents')}
- {pkg.overrides.map(id => {id} )}
-
- )}
- > : null}
-
-
+
busy || rowBusy(row), onSetEnabled: onSetRowEnabled } : undefined}
+ configure={configure}
+ />
{renderSlot('plugins.detail.section', { subject })}
@@ -1511,7 +1442,6 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
{loaded && openPkg !== undefined && openRow === undefined
? (
void
}
-/** Bundle facts supplied to its usage guidance and entry actions. */
-export interface PluginBundleUsageProps {
- /** The bundle whose detail page is open. */
- readonly pkg: PluginPackageRef
-}
-
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
/** Additional MenuItemButton rows after installation in the add-plugin menu. */
'plugins.add.actions': { kind: 'list'; scope: 'root'; owner: PluginAddActionsProps }
- /** Bundle-owned usage guidance below the description, keyed by npm package name. */
- 'plugins.bundle.usage': { kind: 'keyed'; scope: 'root'; owner: PluginBundleUsageProps }
/** Optional guidance after the user enables a bundle from the list, keyed by npm package name. */
'plugins.bundle.activation': { kind: 'keyed'; scope: 'root'; owner: PluginActivationOwnerProps }
/**
diff --git a/packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx b/packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx
index dcacad0820..8d5be40efa 100644
--- a/packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx
+++ b/packages/client/ui-plugin-manager/tests/browser-plugin.client.spec.tsx
@@ -62,30 +62,6 @@ function declare(slots: SlotRegistry): () => void {
}
describe('ui-plugin-manager browser plugin', () => {
- it('reattaches add-plugin menu actions and bundle usage across page declaration lifetimes', async () => {
- const b = await bench()
- b.slots.inject('plugins.add.actions', () => b.slots.register({
- name: 'plugins.add.actions', id: 'create-plugin',
- }, () => null))
- b.slots.inject('plugins.bundle.usage', () => b.slots.register({
- name: 'plugins.bundle.usage', key: '@acme/notes',
- }, () => null))
- const fiber = b.ctx.plugin({ inject: [...inject], apply })
- await fiber.await()
- const removeRoot = declare(b.slots)
- expect(b.slots.entries('plugins.add.actions')).toHaveLength(1)
- expect(b.slots.entries('plugins.bundle.usage')).toHaveLength(1)
- removeRoot()
- expect(b.slots.entries('plugins.add.actions')).toHaveLength(0)
- expect(b.slots.entries('plugins.bundle.usage')).toHaveLength(0)
- declare(b.slots)
- expect(b.slots.entries('plugins.add.actions')).toHaveLength(1)
- expect(b.slots.entries('plugins.bundle.usage')).toHaveLength(1)
- await fiber.dispose()
- expect(b.slots.entries('plugins.add.actions')).toHaveLength(0)
- expect(b.slots.entries('plugins.bundle.usage')).toHaveLength(0)
- })
-
it('resets bundle selection when leaving Plugins and releases its panel observer with the registration', async () => {
const b = await bench()
const removeRoot = declare(b.slots)
@@ -171,9 +147,8 @@ describe('ui-plugin-manager browser plugin', () => {
expect(icon.locale).toBe(NS)
expect(resolveSlotLabel(icon.options.label)).toBe('插件')
// The page declares the slots a plugin's configuration arrives through, and binds their projection beside its state.
- expect(b.slots.spec('plugins.item')).toMatchObject({ kind: 'list', scope: 'root' })
expect(b.slots.spec('plugins.add.actions')).toMatchObject({ kind: 'list', scope: 'root' })
- expect(b.slots.spec('plugins.bundle.usage')).toMatchObject({ kind: 'keyed', scope: 'root' })
+ expect(b.slots.spec('plugins.item')).toMatchObject({ kind: 'list', scope: 'root' })
expect(b.slots.spec('plugins.bundle.config')).toMatchObject({ kind: 'keyed', scope: 'root' })
expect(b.slots.spec('plugins.row.config')).toMatchObject({ kind: 'keyed', scope: 'root' })
for (const name of ['plugins.detail.actions', 'plugins.detail.badge', 'plugins.detail.section'] as const) {
diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
index ec9b88a8c8..b9ebfef8eb 100644
--- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
+++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx
@@ -17,7 +17,7 @@ import type { PluginManagerPageProps } from '../src/client/index.ts'
import type { ConfigLedger } from '../src/client/config-ledger.ts'
import { rowKey, type InstallState, type PackageRow, type PackageView, type PluginManagerState } from '../src/client/manager-store.ts'
import { en, zh, type PluginManagerLocaleKey } from '../src/client/locales.ts'
-import type { PluginActivationOwnerProps, PluginAddActionsProps, PluginBundleUsageProps, PluginDetailProps, PluginsSubject } from '../src/client/slot-contract.ts'
+import type { PluginActivationOwnerProps, PluginAddActionsProps, PluginDetailProps, PluginsSubject } from '../src/client/slot-contract.ts'
afterEach(cleanup)
@@ -37,7 +37,6 @@ function pkg(overrides: Partial = {}): PackageView {
optional: false,
enabled: true,
rows: [],
- overrides: [],
...overrides,
}
}
@@ -83,7 +82,7 @@ const READY: PluginManagerState = {
* (a list slot's cell is empty), the view asked for — `detail` for a detail
* contribution — and the owner props.
*/
-type SlotBodies = Record ReactNode>
+type SlotBodies = Record ReactNode>
/** The subject a detail contribution was rendered with. */
function subjectOf(owner: unknown): PluginsSubject | undefined {
@@ -160,7 +159,6 @@ function renderTab(
const body = bodies[`${name}:${opts?.only ?? opts?.entryKey ?? ''}`]
if (body === undefined) return null
if (name === 'plugins.bundle.activation') return body('activation', owner)
- if (name === 'plugins.bundle.usage') return body('usage', owner)
if (name === 'plugins.add.actions') return body('add', owner)
if (name.startsWith('plugins.detail.')) return body('detail', owner)
if (!('view' in owner) || (owner.view !== 'summary' && owner.view !== 'page')) {
@@ -194,34 +192,16 @@ function renderTab(
}
}
-/** Open the technical controls before exercising component-specific behavior. */
-function expandComponents(dict: typeof en = en): void {
- const toggle = screen.getByRole('button', { name: new RegExp(dict.componentsDisclosure) })
- if (toggle.getAttribute('aria-expanded') === 'false') fireEvent.click(toggle)
-}
-
describe('PluginManagerPage', () => {
- it('keeps contributed creation inside Add plugin and usage before folded implementation details', () => {
- const uses: PluginBundleUsageProps[] = []
- const createPlugin = vi.fn()
- const { actions } = renderTab({ packages: [pkg({ meta: { description: 'Browse your project notes.' }, rows: [row({
- meta: { title: 'Project notes', description: 'Read Markdown notes in this workspace.' },
- })] })] }, {}, {
- 'plugins.add.actions:': (_view, owner) => {
- (owner as PluginAddActionsProps).onDismiss()
- createPlugin()
- }}>Create a plugin ,
- 'plugins.bundle.usage:dsh-better-sidebar': (_view, owner) => {
- uses.push(owner as PluginBundleUsageProps)
- return Open project notes
- },
+ it('opens the add-plugin menu with ArrowDown and returns focus to its trigger on Escape', () => {
+ const { actions } = renderTab({}, {}, {
+ 'plugins.add.actions:': (_view, owner) => { (owner as PluginAddActionsProps).onDismiss() }}>Create a plugin ,
})
- expect(screen.queryByRole('menuitem', { name: 'Create a plugin' })).toBeNull()
const add = screen.getByRole('button', { name: en.chooseAddMethod })
add.focus()
expect(fireEvent.keyDown(add, { key: 'Tab' })).toBe(true)
expect(screen.queryByRole('menu')).toBeNull()
- expect(actions.openInstall).not.toHaveBeenCalled()
fireEvent.keyDown(add, { key: 'ArrowDown' })
const install = screen.getByRole('menuitem', { name: new RegExp(en.installExisting) })
const create = screen.getByRole('menuitem', { name: 'Create a plugin' })
@@ -232,28 +212,7 @@ describe('PluginManagerPage', () => {
fireEvent.keyDown(create, { key: 'Escape' })
expect(screen.queryByRole('menu')).toBeNull()
expect(document.activeElement).toBe(add)
- fireEvent.click(add)
- fireEvent.click(screen.getByRole('menuitem', { name: 'Create a plugin' }))
- expect(createPlugin).toHaveBeenCalledOnce()
expect(actions.openInstall).not.toHaveBeenCalled()
- expect(screen.queryByRole('menu')).toBeNull()
- fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expect(screen.queryByRole('button', { name: en.addPlugin })).toBeNull()
- const usage = screen.getByRole('button', { name: 'Open project notes' })
- expect(uses.at(-1)?.pkg).toMatchObject({ name: 'dsh-better-sidebar', enabled: true })
- expect(screen.getByText(en.deploymentScope)).toBeTruthy()
- expect(screen.getByRole('heading', { name: en.capabilitiesTitle })).toBeTruthy()
- expect(screen.getByText('Project notes')).toBeTruthy()
- expect(screen.getByText('Read Markdown notes in this workspace.')).toBeTruthy()
- expect(document.querySelector('[data-plugin-name]')).toBeNull()
- expect(document.querySelector('[data-plugin-row]')).toBeNull()
- const toggle = screen.getByRole('button', { name: new RegExp(en.componentsDisclosure) })
- expect(toggle.getAttribute('aria-expanded')).toBe('false')
- expect(usage.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
- fireEvent.click(toggle)
- expect(toggle.getAttribute('aria-expanded')).toBe('true')
- expect(document.querySelector('[data-plugin-name]')?.textContent).toBe('dsh-better-sidebar')
- expect(screen.getByRole('switch', { name: en.partToggle.replace('{name}', 'Project notes') })).toBeTruthy()
})
it('opens the existing install dialog from the menu and keeps focus on its input', async () => {
@@ -316,37 +275,6 @@ describe('PluginManagerPage', () => {
expect(screen.queryByRole('menu')).toBeNull()
})
- it('deduplicates readable capabilities and reports declared overrides without exposing ids until expanded', () => {
- const { setLanguage } = renderTab({ packages: [pkg({
- rows: [row({ meta: { title: { en: 'Notes', zh: '笔记' }, description: { en: 'Browse notes.', zh: '浏览笔记' } } }),
- row({ rowId: 'duplicate', meta: { title: { en: 'Notes', zh: '笔记' }, description: { en: 'Browse notes.', zh: '浏览笔记' } } }),
- row({ rowId: 'technical' })],
- overrides: ['system-prompt', 'tools'],
- })] })
- fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- const capabilities = document.querySelector('[data-plugin-capabilities]') as HTMLElement
- expect(within(capabilities).getAllByRole('listitem')).toHaveLength(1)
- expect(screen.getByText(en.overridesDescription.replace('{count}', '2'))).toBeTruthy()
- expect(screen.queryByText('system-prompt')).toBeNull()
- setLanguage(zh)
- expect(within(capabilities).getByText('笔记')).toBeTruthy()
- expect(within(capabilities).getByText('浏览笔记')).toBeTruthy()
- fireEvent.click(screen.getByRole('button', { name: new RegExp(zh.componentsDisclosure) }))
- expect(screen.getByText('system-prompt')).toBeTruthy()
- expect(screen.getByText('tools')).toBeTruthy()
- })
-
- it('opens component diagnostics when a running row fails and omits absent capability metadata', () => {
- const { set } = renderTab({ packages: [pkg({ rows: [row()] })] })
- fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expect(document.querySelector('[data-plugin-capabilities]')).toBeNull()
- expect(document.querySelector('[data-plugin-overrides]')).toBeNull()
- expect(document.querySelector('[data-plugin-row]')).toBeNull()
- set({ packages: [pkg({ rows: [row({ phase: 'failed' })] })] })
- expect(screen.getByRole('button', { name: new RegExp(en.componentsDisclosure) }).getAttribute('aria-expanded')).toBe('true')
- expect(document.querySelector('[data-plugin-row]')?.getAttribute('data-state')).toBe('failed')
- })
-
it('opens the requested bundle after its inventory arrives and falls back when it is absent', () => {
const b = renderTab({ status: 'loading' })
act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-better-sidebar' }) })
@@ -644,7 +572,6 @@ describe('PluginManagerPage', () => {
fireEvent.click(screen.getByRole('switch', { name: zh.enableToggle.replace('{name}', title(zh)) }))
expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith(name, false)
fireEvent.click(screen.getByRole('button', { name: zh.openDetail.replace('{name}', title(zh)) }))
- expandComponents(zh)
for (const dict of [zh, en]) {
setLanguage(dict)
expect(screen.getByRole('heading', { level: 3 }).textContent).toBe(title(dict))
@@ -679,7 +606,6 @@ describe('PluginManagerPage', () => {
set({ packages: [bundle] })
expect(image().getAttribute('src')).toBe(icon)
fireEvent.click(screen.getByRole('button', { name: 'View dsh-better-sidebar' }))
- expandComponents()
expect(image().getAttribute('src')).toBe(icon)
const rowImage = document.querySelector('[data-plugin-row] img')!
expect(rowImage.getAttribute('src')).toBe(icon)
@@ -707,7 +633,6 @@ describe('PluginManagerPage', () => {
fireEvent.click(enable)
expect(actions.setEnabled).toHaveBeenCalledExactlyOnceWith('dsh-better-sidebar', true)
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expandComponents(en)
expect(screen.getByText(en.metadataError.replace('{error}', error))).toBeTruthy()
const uninstall = screen.getByRole('button', { name: en.uninstallLabel.replace('{name}', 'dsh-better-sidebar') })
expect(uninstall).toHaveProperty('disabled', false)
@@ -787,7 +712,6 @@ describe('PluginManagerPage', () => {
{ 'plugins.row.config:dsh-better-sidebar#theme': view => view === 'page' ? : null },
)
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'Bundle title') }))
- expandComponents(en)
expect(screen.getByRole('switch', { name: en.partToggle.replace('{name}', '@acme/dsh-sidebar/navigation') })).toBeTruthy()
expect(screen.getByRole('switch', { name: en.partToggle.replace('{name}', '@acme/dsh-theme') })).toBeTruthy()
expect(screen.queryByText('中文主题说明。')).toBeNull()
@@ -798,7 +722,7 @@ describe('PluginManagerPage', () => {
expect(within(navigation).queryByText('Bundle description.')).toBeNull()
fireEvent.click(within(navigation).getByRole('switch', { name: zh.partToggle.replace('{name}', '@acme/dsh-sidebar/navigation') }))
expect(actions.setRowEnabled).toHaveBeenCalledExactlyOnceWith('include:sidebar', false)
- expect(within(document.querySelector('[data-plugin-rows]') as HTMLElement).getByText('中文主题说明。')).toBeTruthy()
+ expect(screen.getByText('中文主题说明。')).toBeTruthy()
fireEvent.click(screen.getByRole('button', { name: zh.configureRow.replace('{name}', '主题插件') }))
expect(document.querySelector('[data-plugin-row-detail]')?.getAttribute('data-plugin-row-detail')).toBe('dsh-better-sidebar#theme')
expect(screen.getByRole('heading', { level: 3 }).textContent).toBe('主题插件')
@@ -819,7 +743,6 @@ describe('PluginManagerPage', () => {
{ 'plugins.row.config:dsh-better-sidebar#sidebar': view => view === 'page' ? : null },
)
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expandComponents(en)
const listed = document.querySelector('[data-plugin-row="include:sidebar"]') as HTMLElement
expect(within(listed).getByText('Sidebar component')).toBeTruthy()
expect(within(listed).getByText('sidebar', { selector: 'code' })).toBeTruthy()
@@ -868,7 +791,7 @@ describe('PluginManagerPage', () => {
}
expect(screen.getByText('sidebar')).toBeTruthy()
expect(screen.getByText('@acme/dsh-sidebar-widget')).toBeTruthy()
- expect(within(document.querySelector('[data-plugin-rows]') as HTMLElement).getByText('侧边导航')).toBeTruthy()
+ expect(screen.getByText('侧边导航')).toBeTruthy()
expect(screen.getByText(zh.metadataError.replace('{error}', error))).toBeTruthy()
const toggle = screen.getByRole('switch', { name: zh.partToggle.replace('{name}', '导航组件') })
expect(toggle).toHaveProperty('disabled', false)
@@ -950,7 +873,6 @@ describe('PluginManagerPage', () => {
const theme = row({ rowId: 'theme', moduleName: 'dsh-better-sidebar/theme', entryId: 'include:theme' as PluginEntryId })
renderTab({ packages: [pkg({ rows: [row(), theme] })] }, { rows: new Set(['dsh-better-sidebar#sidebar']) }, bodies)
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expandComponents(en)
expect(screen.queryByRole('button', { name: en.configureRow.replace('{name}', 'dsh-better-sidebar/theme') })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: en.configureRow.replace('{name}', 'dsh-better-sidebar') }))
const page = document.querySelector('[data-plugin-row-detail="dsh-better-sidebar#sidebar"]') as HTMLElement
@@ -993,7 +915,6 @@ describe('PluginManagerPage', () => {
expect(screen.queryByRole('button', { name: /^act / })).toBeNull()
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expandComponents(en)
const detail = document.querySelector('[data-plugin-detail]') as HTMLElement
const act = within(detail).getByRole('button', { name: 'act bundle dsh-better-sidebar' })
expect(within(detail).getByText('badge bundle dsh-better-sidebar')).toBeTruthy()
@@ -1027,7 +948,7 @@ describe('PluginManagerPage', () => {
})
it('leaves the version out of a bundle the Host reports none for', () => {
- const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, optional: false, enabled: true, rows: [], overrides: [] }
+ const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, optional: false, enabled: true, rows: [] }
renderTab({ packages: [unversioned] }, {}, bodies)
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
expect(subjects.at(-1)).toEqual({ kind: 'bundle', pkg: { name: 'dsh-better-sidebar', installed: true, enabled: true, rows: [] } })
@@ -1039,7 +960,6 @@ describe('PluginManagerPage', () => {
const { setLanguage } = renderTab({ packages: [pkg({ name, meta: { description: { en: 'Third-party description.' } } })] })
setLanguage(zh)
fireEvent.click(screen.getByRole('button', { name: zh.openDetail.replace('{name}', name) }))
- expandComponents(zh)
expect(screen.getByRole('heading', { level: 3 }).textContent).toBe(name)
expect(screen.getByText('Third-party description.')).toBeTruthy()
expect(document.querySelector('[data-plugin-name]')?.textContent).toBe(name)
@@ -1098,7 +1018,6 @@ describe('PluginManagerPage', () => {
})],
})
fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') }))
- expandComponents(en)
const detail = document.querySelector('[data-plugin-detail="dsh-better-sidebar"]') as HTMLElement
expect(within(detail).getByRole('heading', { level: 3 }).textContent).toBe('dsh-better-sidebar')
// The version sits beside the name as a tag; the crumb only leads back.
diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
index 9dc32dd5cb..895a4feccd 100644
--- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
+++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts
@@ -113,15 +113,10 @@ it('hands a custom page the shared configuration form of its entry', () => {
})
describe('packageView', () => {
- it('retains the Host-reported rows adjusted by the bundle', () => {
- expect(packageView({ ...BUNDLE, overrides: ['system-prompt', 'tools'] }, PLUGINS).overrides)
- .toEqual(['system-prompt', 'tools'])
- })
it('joins a bundle with the entries its rows run as', () => {
expect(packageView(BUNDLE, PLUGINS)).toEqual({
name: 'dsh-better-sidebar', version: '0.16.0', description: 'A sidebar.',
installed: true, optional: false, enabled: false,
- overrides: [],
rows: [
{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY, enabled: true, phase: 'active' },
{ rowId: 'theme', moduleName: 'dsh-better-sidebar/theme', enabled: false, phase: null },
@@ -137,7 +132,6 @@ describe('packageView', () => {
expect(packageView(protectedBundle, PLUGINS)).toEqual({
name: '@deepseek-ai/dsh-base', installed: false, optional: false, enabled: true, readOnlyReason: 'management-required',
error: { code: 'operation-error', diagnostic: 'broken' },
- overrides: [],
rows: [
{ rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core', enabled: true, phase: 'active', readOnlyReason: 'management-required' },
{ rowId: 'gone', moduleName: 'x', entryId: 'include:gone', enabled: false, phase: null },
diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml
index 162afe69f9..8866d6e127 100644
--- a/packages/client/ui-schedule/README.i18n.yaml
+++ b/packages/client/ui-schedule/README.i18n.yaml
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 405b30feaf48c087
/deepseek-ai-dsh-client-ui-schedule/use-this-package:
- en: b318072ec2191a71
- zh: adf9a59896141732
+ en: c129ae5d0a150a5d
+ zh: d81976f2840cb059
/deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source:
en: 6bd6882dbbbefcb0
zh: 03aa15f1a65d13dc
diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md
index 9fda3f37cf..ed9431a6b5 100644
--- a/packages/client/ui-schedule/README.md
+++ b/packages/client/ui-schedule/README.md
@@ -25,8 +25,6 @@ Manage active and inactive tasks from the Automation tasks page and the right Si
## Use this package
-The enabled Schedule bundle's detail page offers a usage card with its capability, sidebar location, and Open automation tasks action. Explicit enablement also opens dismissible guidance. Both reuse the Host catalog to confirm availability before enabling navigation, show read failures with Retry, and disappear when this plugin unloads. Opening the task page creates no Session or task.
-
The shipped Web composition carries no `ui-schedule` row; enable the optional experimental bundle `@deepseek-ai/dsh-experimental-schedule-bundle` from the Plugins page (Official group), or list it in a profile's `dsh.profile.bundles`, to insert and mount `ui-schedule` with the Host Schedule service and the clock context that supplies the model's current time. The Automation tasks sidebar entry opens a global page without selecting or activating a Session. The page header's New action starts a new Session, where the reminder is created by asking the model; the page itself has no creation form. Its task list supports text search and one All, Enabled, and Inactive status filter row; search matches the stored task name, the instruction, and the internal Session id, not a resolved Session title, and the empty state keeps the heading's New action beside the notice. Selecting an active or inactive task opens a second detail level with Rules and Delivery records tabs. Every list row shows the task's stored title with its status, frequency, and next run and never the original Session name; the detail's name control shows the stored title. Rules contains the complete reminder, status, next target for active tasks, original recurrence rule, and task id. Daily rule labels show the saved local time and exact stored IANA zone, preserving nonzero seconds and milliseconds rather than substituting the browser's zone. The Linked session entry in the detail tab strip names the linked Session by its catalog title, falling back to the Session id when no title is available. It opens an available, unarchived Session after metadata loads; pending Session or Workspace metadata, an archived Session, missing Session membership, or a Workspace query error keeps it disabled with an explanation. The click rechecks availability; browsing does not unarchive the Session. Only this action requests Session activation. Deletion is available from the More task actions menu in the detail's top strip, separate from navigation.
Selecting a task from the list opens Rules; refreshing that task preserves the selected tab. Rules edits an active task's name, instruction, and run time; the edits stay in a local draft until saved, and inactive tasks remain read-only. Unmodified Left/Right arrows cycle the tabs, Home/End select the first/last tab, and keyboard focus follows the selection. The tabs do not consume these keys with Alt, Ctrl, Meta, or Shift. The linked-Session control in the detail tab strip stays available on the Rules tab, and confirmed deletion is available from either view.
diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md
index dab4ae922b..d03aefa658 100644
--- a/packages/client/ui-schedule/README.zh.md
+++ b/packages/client/ui-schedule/README.zh.md
@@ -25,8 +25,6 @@ kind: "package-reference"
## 使用本包
-已启用的 Schedule 组合包在详情页提供使用卡片,展示能力、侧栏入口和「打开自动化任务」操作;显式启用后还会显示可关闭的引导。两处均复用 Host 任务目录查询,在确认可用后开放跳转,读取失败时显示重试,并在本插件卸载后移除。打开任务页不会创建 Session 或任务。
-
随发行版交付的 Web 组合不含 `ui-schedule` 行;在插件管理页的“官方”分组中启用可选实验性 bundle `@deepseek-ai/dsh-experimental-schedule-bundle`,或把它列进 profile 的 `dsh.profile.bundles`,即可插入 `ui-schedule` 并将它与 Host Schedule 服务、以及为模型提供当前时间的时钟上下文一起挂载。侧栏的自动化任务入口打开全局页面,不选择或激活会话。页面页头的“新建”操作会打开一个新会话,提醒由模型在该会话中创建;页面本身没有创建表单。列表支持文本搜索和一行“全部/已开启/未运行”状态筛选;搜索匹配任务已存储的名称、指令和内部会话 id,不匹配解析出的会话标题,空状态在提示旁保留页头的“新建”操作。选择活动或未运行任务后,进入含“规则”和“任务运行记录”页签的第二层详情。每条列表行显示任务已存储的标题及其状态、频率和下次运行,不显示原会话名称;详情中的名称输入框显示任务已存储的标题。“规则”显示完整提醒、状态、活动任务的下一目标时间、原始重复规则和任务 id。每天的规则标签显示已保存的本地时间与精确的存储 IANA 时区,保留非零秒与毫秒,而不替换为浏览器时区。详情页签条内的“关联会话”入口按会话标题显示,取不到标题时回退显示会话 id。元数据加载后,它可打开可用且未归档的会话;会话或 Workspace 元数据尚未就绪、会话已归档、会话不在列表中或 Workspace 查询失败时,入口保持禁用并显示原因。点击时会重新检查可用性;浏览不会取消会话归档。仅此操作请求激活会话。删除位于详情顶部条带的“更多任务操作”菜单。
从列表选择任务时默认打开“规则”;刷新同一任务会保留当前页签。活动任务的“规则”修改名称、指令和运行时间;这些修改保留在本地草稿中直到保存,未运行任务保持只读。不带修饰键的左右方向键循环切换页签,Home/End 选择第一个/最后一个页签,键盘焦点随选择移动。页签不会拦截这些键与 Alt、Ctrl、Meta 或 Shift 的组合。共用的会话入口位于详情页签条内,在“规则”页签可见;确认删除操作在两种页签下都可用。
diff --git a/packages/client/ui-schedule/package.json b/packages/client/ui-schedule/package.json
index edb8398a79..665d1bca90 100644
--- a/packages/client/ui-schedule/package.json
+++ b/packages/client/ui-schedule/package.json
@@ -62,7 +62,6 @@
"@deepseek-ai/dsh-client-test-runtime": "workspace:*",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:*",
"@deepseek-ai/dsh-client-ui-layout": "workspace:*",
- "@deepseek-ai/dsh-client-ui-plugin-manager": "workspace:*",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:*",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-session": "workspace:*",
diff --git a/packages/client/ui-schedule/src/client/SchedulePluginUsage.module.css b/packages/client/ui-schedule/src/client/SchedulePluginUsage.module.css
deleted file mode 100644
index 8f96f18b4f..0000000000
--- a/packages/client/ui-schedule/src/client/SchedulePluginUsage.module.css
+++ /dev/null
@@ -1,53 +0,0 @@
-.card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 16px;
- padding: 16px;
- border: 0.5px solid var(--dsw-alias-border-l3);
- border-radius: var(--dsw-radius-lg);
- color: var(--dsw-alias-label-primary);
- font-size: 13px;
- line-height: 20px;
-}
-
-.summary {
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-width: 0;
-}
-
-.card .summary {
- flex: 1 1 240px;
-}
-
-.status {
- display: flex;
- align-items: center;
- gap: 8px;
- font-weight: 500;
-}
-
-.description {
- margin: 0;
- color: var(--dsw-alias-label-secondary);
- font-size: 13px;
- line-height: 20px;
-}
-
-.location {
- display: flex;
- flex-wrap: wrap;
- gap: 4px 12px;
- color: var(--dsw-alias-label-secondary);
- font-size: 13px;
- line-height: 20px;
-}
-
-.actions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-}
diff --git a/packages/client/ui-schedule/src/client/SchedulePluginUsage.tsx b/packages/client/ui-schedule/src/client/SchedulePluginUsage.tsx
deleted file mode 100644
index 86d114e40a..0000000000
--- a/packages/client/ui-schedule/src/client/SchedulePluginUsage.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-/** Navigation from the enabled Schedule bundle to its live task page. */
-import { Button, Modal, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
-import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'
-import type { ScheduleCatalogEntry } from '@deepseek-ai/dsh-schedule/client'
-import type { CatalogInjected, CatalogSnapshot } from './catalog-source.ts'
-import css from './SchedulePluginUsage.module.css'
-
-/** Shared catalog readiness and navigation supplied by the Schedule plugin. */
-export interface SchedulePluginUsageInjected extends Pick, 'hooks' | 'onRetry'> {
- /** Open the global task page without selecting or creating a Session. */
- readonly onOpenAutomation: () => void
-}
-
-/** Shares for the bundle detail's usage card. */
-export type SchedulePluginUsageProps = PropsRuntime<'plugins.bundle.usage'>
- & PropsLocale<'schedule.manager'> & InjectFace
-
-/** Shares for the guidance shown after explicit bundle enablement. */
-export type SchedulePluginActivationProps = PropsRuntime<'plugins.bundle.activation'>
- & PropsLocale<'schedule.manager'> & InjectFace
-
-type Status = CatalogSnapshot['status']
-type CopyProps = PropsLocale<'schedule.manager'> & { readonly status: Status }
-
-function statusKey(status: Status) {
- return status === 'ready' ? 'plugin.ready' : status === 'loading' ? 'plugin.checking' : 'plugin.unavailable'
-}
-
-function UsageSummary({ status, t }: CopyProps) {
- return
-
-
- {t(statusKey(status))}
-
-
{t('plugin.capability')}
-
- {t('plugin.locationLabel')}
- {t('plugin.location')}
-
-
-}
-
-/**
- * Show an enabled bundle's destination once the Host catalog answers.
- * @param props - bundle enablement, catalog hook, navigation and localized copy.
- * @returns a usage card, or nothing for a disabled bundle.
- */
-export function SchedulePluginUsage({ pkg, useCatalog, onOpenAutomation, onRetry, t }: SchedulePluginUsageProps) {
- const status = useCatalog(snapshot => snapshot.status)
- if (!pkg.enabled) return null
- return
-
-
- {status === 'error' ? { void onRetry() }}>{t('list.retry')} : null}
- {t('plugin.open')}
-
-
-}
-
-/**
- * Guide an explicit activation to the same task page after the Host answers.
- * @param props - catalog hook, dismiss and navigation callbacks, and localized copy.
- * @returns dismissible guidance without creating a task or starting a model turn.
- */
-export function SchedulePluginActivation({ useCatalog, onOpenAutomation, onRetry, onDismiss, t }: SchedulePluginActivationProps) {
- const status = useCatalog(snapshot => snapshot.status)
- return
- {t('plugin.close')}
- {status === 'error' ? { void onRetry() }}>{t('list.retry')} : null}
- { onDismiss(); onOpenAutomation() }}>{t('plugin.open')}
- >}>
-
-
-}
diff --git a/packages/client/ui-schedule/src/client/index.ts b/packages/client/ui-schedule/src/client/index.ts
index 1d699fc417..084d0edb8d 100644
--- a/packages/client/ui-schedule/src/client/index.ts
+++ b/packages/client/ui-schedule/src/client/index.ts
@@ -57,7 +57,6 @@ import { TaskTabBindings } from './task-tab-bindings.ts'
import { TaskManagerPage, type TaskManagerInjected } from './TaskManagerPage.tsx'
import type { TaskDetailInjected } from './TaskDetail.tsx'
import { TaskManagerIcon } from './TaskManagerIcon.tsx'
-import { SchedulePluginActivation, SchedulePluginUsage, type SchedulePluginUsageInjected } from './SchedulePluginUsage.tsx'
import { sessionLinkState } from './session-link.ts'
import { en, NS, zh, type ScheduleCatalogKey } from './locales.ts'
import { en as managerEn, zh as managerZh, type TaskManagerKey } from './task-manager-locales.ts'
@@ -183,19 +182,6 @@ export function apply(ctx: ClientContext): void {
locale: MANAGER_NS,
label: () => t('panel'),
}, TaskManagerIcon))
- ctx.inject(['layout'], (scope) => {
- const usage = (): SchedulePluginUsageInjected => ({
- hooks: manager.hooks,
- onRetry: manager.onRetry,
- onOpenAutomation: () => { scope.layout.selectPanel(PANEL_ID) },
- })
- scope.slots.inject('plugins.bundle.usage', () => scope.slots.register({
- name: 'plugins.bundle.usage', key: '@deepseek-ai/dsh-experimental-schedule-bundle', locale: MANAGER_NS, inject: usage,
- }, SchedulePluginUsage))
- scope.slots.inject('plugins.bundle.activation', () => scope.slots.register({
- name: 'plugins.bundle.activation', key: '@deepseek-ai/dsh-experimental-schedule-bundle', locale: MANAGER_NS, inject: usage,
- }, SchedulePluginActivation))
- })
// The created task is a Turn-level element, not a Tool-group row: the Turn
// Definition publishes the settled result and this tail list entry renders
// the card beneath the closing prose.
diff --git a/packages/client/ui-schedule/src/client/task-manager-locales.ts b/packages/client/ui-schedule/src/client/task-manager-locales.ts
index 83ce50f1c1..6906e54fff 100644
--- a/packages/client/ui-schedule/src/client/task-manager-locales.ts
+++ b/packages/client/ui-schedule/src/client/task-manager-locales.ts
@@ -5,15 +5,6 @@ import { frequencyEn, frequencyZh } from './frequency-locales.ts'
export const en = {
'panel': 'Automation tasks',
'title': 'Automation tasks',
- 'plugin.usageLabel': 'Use automation tasks',
- 'plugin.ready': 'Automation tasks are ready to use',
- 'plugin.checking': 'Checking automation tasks…',
- 'plugin.unavailable': 'Automation tasks are currently unavailable',
- 'plugin.capability': 'Continue the original session at a scheduled time or on a repeating schedule.',
- 'plugin.locationLabel': 'Location',
- 'plugin.location': 'Sidebar · Automation tasks',
- 'plugin.open': 'Open automation tasks',
- 'plugin.close': 'Close',
'new.action': 'New',
'list.label': 'Task catalog',
'list.nextPrefix': 'Next scheduled time: ',
@@ -157,15 +148,6 @@ export type TaskManagerKey = keyof typeof en
export const zh: Record = {
'panel': '自动化任务',
'title': '自动化任务',
- 'plugin.usageLabel': '使用自动化任务',
- 'plugin.ready': '自动化任务已可使用',
- 'plugin.checking': '正在确认自动化任务…',
- 'plugin.unavailable': '自动化任务暂不可用',
- 'plugin.capability': '按时间或周期继续原会话',
- 'plugin.locationLabel': '入口位置',
- 'plugin.location': '侧栏 · 自动化任务',
- 'plugin.open': '打开自动化任务',
- 'plugin.close': '关闭',
'new.action': '新建',
'list.label': '任务列表',
'list.nextPrefix': '下次计划时间:',
diff --git a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts
index b73dfb5c3c..4fa73699b9 100644
--- a/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts
+++ b/packages/client/ui-schedule/tests/browser-plugin.client.spec.ts
@@ -3,7 +3,7 @@ import { Context, Service } from '@deepseek-ai/cordis'
import { describe, expect, it, vi, type Mock } from 'vitest'
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
import { UiConversation } from '@deepseek-ai/dsh-client-ui-conversation/client'
-import { resolveSlotLabel, type HostObservable, type PropsRenderSlots, type StoredEntry } from '@deepseek-ai/dsh-client-ui-slots'
+import { resolveSlotLabel, type HostObservable, type StoredEntry } from '@deepseek-ai/dsh-client-ui-slots'
import { stubConfigForm } from '@deepseek-ai/dsh-client-test-runtime'
import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
import { apply, inject } from '../src/client/index.ts'
@@ -24,11 +24,9 @@ import { ScheduleCatalogAction } from '../src/client/ScheduleCatalogAction.tsx'
import { SessionScheduleHover } from '../src/client/SessionScheduleHover.tsx'
import { SessionScheduleMark } from '../src/client/SessionScheduleMark.tsx'
import { ScheduleTurnCard, type ScheduleTurnCardInjected } from '../src/client/ScheduleTurnCard.tsx'
-import { SchedulePluginActivation, SchedulePluginUsage, type SchedulePluginUsageInjected } from '../src/client/SchedulePluginUsage.tsx'
import { en, NS, zh } from '../src/client/locales.ts'
const Empty = () => null
-const PluginSlots = (_props: PropsRenderSlots<'plugins.bundle.usage' | 'plugins.bundle.activation'>) => null
/** Raw inject face of an ambient row seat: the shared Host catalog observable. */
interface SessionCatalogFace {
@@ -139,44 +137,6 @@ function declareTranscriptSeats(ctx: Context): () => void {
}
describe('ui-schedule browser half', () => {
- it('contributes plugin guidance only while loaded and opens the real task panel without creating a Session', async () => {
- const catalog = vi.fn().mockResolvedValue({ ok: true, value: [] })
- const ctx = await baseContext({ schedule: { catalog }, $on: () => () => {} })
- const selectPanel = vi.fn()
- const startSession = vi.spyOn(ctx.uiWorkspace, 'startSession')
- const openSession = vi.spyOn(ctx.uiWorkspace, 'openSession')
- ctx.provide('layout', { selectPanel } as never)
- const fiber = ctx.plugin({ inject: [...inject], apply })
- await fiber.await()
- expect(ctx.slots.entries('plugins.bundle.usage')).toHaveLength(0)
- const declaration = ctx.slots.register({ name: 'root', children: {
- 'plugins.bundle.usage': { kind: 'keyed', scope: 'root' },
- 'plugins.bundle.activation': { kind: 'keyed', scope: 'root' },
- } }, PluginSlots)
- const usage = ctx.slots.entries('plugins.bundle.usage')[0]!
- const activation = ctx.slots.entries('plugins.bundle.activation')[0]!
- expect(usage.options.key).toBe('@deepseek-ai/dsh-experimental-schedule-bundle')
- expect(usage.component).toBe(SchedulePluginUsage)
- expect(activation.component).toBe(SchedulePluginActivation)
- const face = injectedFace(usage) as SchedulePluginUsageInjected
- const guidance = injectedFace(activation) as SchedulePluginUsageInjected
- expect(guidance.hooks.catalog).toBe(face.hooks.catalog)
- const stop = face.hooks.catalog.subscribe(vi.fn())
- await face.onRetry()
- expect(face.hooks.catalog.getSnapshot().status).toBe('ready')
- face.onOpenAutomation()
- guidance.onOpenAutomation()
- expect(selectPanel.mock.calls).toEqual([['schedules'], ['schedules']])
- expect(startSession).not.toHaveBeenCalled()
- expect(openSession).not.toHaveBeenCalled()
- stop()
- await fiber.dispose()
- expect(ctx.slots.entries('plugins.bundle.usage')).toHaveLength(0)
- expect(ctx.slots.entries('plugins.bundle.activation')).toHaveLength(0)
- declaration()
- await ctx.fiber.dispose()
- })
-
it('declares only the services used by registration', () => {
expect(inject).toEqual([
'slots', 'locale', 'remote', 'remote.schedule', 'conversation', 'uiConversation', 'uiWorkspace', 'sessions',
diff --git a/packages/client/ui-schedule/tests/schedule-plugin-usage.client.spec.tsx b/packages/client/ui-schedule/tests/schedule-plugin-usage.client.spec.tsx
deleted file mode 100644
index c06622bf0d..0000000000
--- a/packages/client/ui-schedule/tests/schedule-plugin-usage.client.spec.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-// @vitest-environment jsdom
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
-import { afterEach, describe, expect, it, vi } from 'vitest'
-import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
-import type { ScheduleCatalogEntry } from '@deepseek-ai/dsh-schedule/client'
-import type { CatalogSnapshot } from '../src/client/catalog-source.ts'
-import {
- SchedulePluginActivation, SchedulePluginUsage,
- type SchedulePluginActivationProps, type SchedulePluginUsageProps,
-} from '../src/client/SchedulePluginUsage.tsx'
-import { en, zh } from '../src/client/task-manager-locales.ts'
-
-afterEach(cleanup)
-
-function props(status: CatalogSnapshot['status'] = 'ready', enabled = true): SchedulePluginUsageProps {
- const snapshot: CatalogSnapshot = {
- status, records: [], deleting: [], settled: status === 'ready', readRequest: 1,
- readSettled: status === 'ready' ? 1 : 0,
- }
- return {
- pkg: { name: '@deepseek-ai/dsh-experimental-schedule-bundle', installed: false, enabled, rows: [] },
- useCatalog: selector => selector(snapshot),
- onRetry: vi.fn().mockResolvedValue(undefined),
- onOpenAutomation: vi.fn(),
- t: makeTranslate(en),
- } as SchedulePluginUsageProps
-}
-
-function activation(status: CatalogSnapshot['status'] = 'ready'): SchedulePluginActivationProps {
- return {
- ...props(status), packageName: '@deepseek-ai/dsh-experimental-schedule-bundle',
- onDismiss: vi.fn(), onOpenDetails: vi.fn(),
- }
-}
-
-describe('Schedule plugin usage', () => {
- it('explains the live capability and opens the task page only on a click', () => {
- const value = props()
- render( )
- expect(screen.getByText('Automation tasks are ready to use')).toBeTruthy()
- expect(screen.getByText('Sidebar · Automation tasks')).toBeTruthy()
- expect(screen.getByText('Continue the original session at a scheduled time or on a repeating schedule.')).toBeTruthy()
- expect(value.onOpenAutomation).not.toHaveBeenCalled()
- fireEvent.click(screen.getByRole('button', { name: 'Open automation tasks' }))
- expect(value.onOpenAutomation).toHaveBeenCalledOnce()
- })
-
- it('hides an explicitly disabled bundle and localizes a ready bundle', () => {
- const value = props('ready', false)
- const view = render( )
- expect(screen.queryByRole('region')).toBeNull()
- view.rerender( )
- expect(screen.getByText('自动化任务已可使用')).toBeTruthy()
- expect(screen.getByText('侧栏 · 自动化任务')).toBeTruthy()
- expect(screen.getByRole('button', { name: '打开自动化任务' })).toBeTruthy()
- })
-
- it('does not announce readiness until a catalog query succeeds and retries failures', () => {
- const loading = props('loading')
- const view = render( )
- expect(screen.getByText('Checking automation tasks…')).toBeTruthy()
- fireEvent.click(screen.getByRole('button', { name: 'Open automation tasks' }))
- expect(loading.onOpenAutomation).not.toHaveBeenCalled()
- const failed = props('error')
- view.rerender( )
- expect(screen.getByText('Automation tasks are currently unavailable')).toBeTruthy()
- expect(screen.queryByText('Automation tasks are ready to use')).toBeNull()
- fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
- expect(failed.onRetry).toHaveBeenCalledOnce()
- fireEvent.click(screen.getByRole('button', { name: 'Open automation tasks' }))
- expect(failed.onOpenAutomation).not.toHaveBeenCalled()
- })
-})
-
-describe('Schedule activation guidance', () => {
- it('dismisses the guidance before opening the task page', () => {
- const value = activation()
- const calls: string[] = []
- render( { calls.push('close') }} onOpenAutomation={() => { calls.push('open') }} />)
- expect(screen.getByRole('dialog', { name: 'Use automation tasks' })).toBeTruthy()
- expect(calls).toEqual([])
- fireEvent.click(screen.getByRole('button', { name: 'Open automation tasks' }))
- expect(calls).toEqual(['close', 'open'])
- })
-
- it('keeps failed activation guidance dismissible with a retry and no ready claim', () => {
- const value = activation('error')
- render( )
- expect(screen.getByText('Automation tasks are currently unavailable')).toBeTruthy()
- fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
- expect(value.onRetry).toHaveBeenCalledOnce()
- fireEvent.click(screen.getByRole('button', { name: 'Open automation tasks' }))
- expect(value.onOpenAutomation).not.toHaveBeenCalled()
- fireEvent.click(screen.getAllByRole('button', { name: 'Close' })[0]!)
- expect(value.onDismiss).toHaveBeenCalledOnce()
- })
-})
diff --git a/packages/client/ui-schedule/tsconfig.json b/packages/client/ui-schedule/tsconfig.json
index bed6d290b6..09401d4291 100644
--- a/packages/client/ui-schedule/tsconfig.json
+++ b/packages/client/ui-schedule/tsconfig.json
@@ -29,9 +29,6 @@
{
"path": "../ui-layout"
},
- {
- "path": "../ui-plugin-manager/tsconfig.client.json"
- },
{
"path": "../ui-sidebar"
},
diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
index 0784651381..be8dfdb1d5 100644
--- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
+++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts
@@ -1848,7 +1848,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.add.actions\', () => ctx.slots.register(\n { name: \'plugins.add.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:90',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:84',
},
{
key: 'plugins.bundle.activation',
@@ -1877,17 +1877,16 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
'useSessionRetainInfo: UseSessionRetainInfo',
'useWorkspaces: SnapshotSelectorHook',
],
- keyDomain: 'open: any string the owner dispatches (no compile-time key set), already taken: @deepseek-ai/dsh-experimental-schedule-bundle, @deepseek-ai/dsh-experimental-voice-input-bundle',
+ keyDomain: 'open: any string the owner dispatches (no compile-time key set), already taken: @deepseek-ai/dsh-experimental-voice-input-bundle',
hookContext: '',
slotInject: '',
declaredBy: 'an entry in \'main\' (client-ui-plugin-manager), so it exists while that entry is mounted',
occupants: [
- 'client-ui-schedule SchedulePluginActivation key \'@deepseek-ai/dsh-experimental-schedule-bundle\'',
'experimental-client-ui-voice-input VoiceSetupPrompt key \'@deepseek-ai/dsh-experimental-voice-input-bundle\'',
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.activation\', () => ctx.slots.register(\n { name: \'plugins.bundle.activation\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:94',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:86',
},
{
key: 'plugins.bundle.config',
@@ -1927,47 +1926,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'shadows-shipped-ui',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.config\', () => ctx.slots.register(\n { name: \'plugins.bundle.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:110',
- },
- {
- key: 'plugins.bundle.usage',
- kind: 'keyed',
- scope: 'root',
- summary: 'Bundle-owned usage guidance below the description, keyed by npm package name.',
- doc: 'Bundle-owned usage guidance below the description, keyed by npm package name.',
- registerOptions: [
- {
- name: 'key',
- requirement: 'required',
- type: 'string',
- doc: 'Your cell key: the entry renders where the owner dispatches this exact key. Registering an already-occupied key replaces that occupant.',
- },
- ],
- ownerProps: [
- '/** Bundle facts supplied to its usage guidance and entry actions. */\nexport interface PluginBundleUsageProps {\n /** The bundle whose detail page is open. */\n readonly pkg: PluginPackageRef\n}',
- ],
- ownerPropsReferences: [
- 'PluginPackageRef',
- ],
- standardProps: [
- 'useResource: UseResource',
- 'useWorkspaces: SnapshotSelectorHook',
- 'usePanelInfo: UsePanelInfo',
- 'useSessions: UseSessions',
- 'useSessionStatus: UseSessionStatus',
- 'useSessionRetainInfo: UseSessionRetainInfo',
- 'useWorkspaces: SnapshotSelectorHook',
- ],
- keyDomain: 'open: any string the owner dispatches (no compile-time key set), already taken: @deepseek-ai/dsh-experimental-schedule-bundle',
- hookContext: '',
- slotInject: '',
- declaredBy: 'an entry in \'main\' (client-ui-plugin-manager), so it exists while that entry is mounted',
- occupants: [
- 'client-ui-schedule SchedulePluginUsage key \'@deepseek-ai/dsh-experimental-schedule-bundle\'',
- ],
- replaceRisk: 'shadows-shipped-ui',
- example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.usage\', () => ctx.slots.register(\n { name: \'plugins.bundle.usage\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:92',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:102',
},
{
key: 'plugins.detail.actions',
@@ -2017,7 +1976,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.actions\', () => ctx.slots.register(\n { name: \'plugins.detail.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:124',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:116',
},
{
key: 'plugins.detail.badge',
@@ -2067,7 +2026,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.badge\', () => ctx.slots.register(\n { name: \'plugins.detail.badge\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:129',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:121',
},
{
key: 'plugins.detail.section',
@@ -2117,7 +2076,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.section\', () => ctx.slots.register(\n { name: \'plugins.detail.section\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:136',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:128',
},
{
key: 'plugins.item',
@@ -2172,7 +2131,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.item\', () => ctx.slots.register(\n { name: \'plugins.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:104',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:96',
},
{
key: 'plugins.row.config',
@@ -2210,7 +2169,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [
occupants: [],
replaceRisk: 'none',
example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.row.config\', () => ctx.slots.register(\n { name: \'plugins.row.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}',
- source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:118',
+ source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:110',
},
{
key: 'rightbar',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2d76978aee..1a2f5d5ad7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4566,9 +4566,6 @@ importers:
'@deepseek-ai/dsh-client-ui-layout':
specifier: workspace:*
version: link:../ui-layout
- '@deepseek-ai/dsh-client-ui-plugin-manager':
- specifier: workspace:*
- version: link:../ui-plugin-manager
'@deepseek-ai/dsh-client-ui-primitives':
specifier: workspace:*
version: link:../ui-primitives
diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md
index f29261d70e..e489e916e6 100644
--- a/snapshots/web/minimal-preset/ui.expected.md
+++ b/snapshots/web/minimal-preset/ui.expected.md
@@ -1,6 +1,6 @@
- banner:
- navigation "Session hierarchy": Use the bash tool to
- - 'button "Mode details: Minimal mode"': Minimal mode
+ - text: Minimal mode
- button "More actions"
- button "Open right sidebar"
- tablist: