Browse Source

refactor(web): build the plugin manager on the shared ui-primitives controls

After master's consolidation, the Manage plugins tab and the preset
detail's Capabilities section use Switch, Tag, Button, Input, and StateDot
from ui-primitives instead of their own copies; only the icon buttons, the
red trash icon, and the component chips stay local, as the catalog has no
answer for them. The three packages the stack adds take the workspace
version master released.
Yichen Jiang 1 month ago
parent
commit
0af7ca4c10

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.md
-2026-09-04-plugin-management-in-web-settings.md: 9d6af8098549f9842c5da3bf323a4cc899f16288
-2026-09-04-plugin-management-in-web-settings.zh.md: cd46b9fc59a54344fae619310a0d4cf047ccdc35
+2026-09-04-plugin-management-in-web-settings.md: ed0ac26e6c3e984dbf137d64e43919a920611f06
+2026-09-04-plugin-management-in-web-settings.zh.md: d8a5d2cda58fc6d87401880e57542a1fd3b23c82

File diff suppressed because it is too large
+ 0 - 1
.agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.md


File diff suppressed because it is too large
+ 0 - 1
.agents/notes/implemented/architecture/2026-09-04-plugin-management-in-web-settings.zh.md


+ 256 - 155
packages/client/ui-settings-plugin-manager/src/client/PluginManagerSettingsTab.module.css

@@ -109,42 +109,11 @@
   margin: -4px 0 4px;
 }
 
-.switcher {
-  display: inline-flex;
-  align-items: center;
-  gap: 8px;
-  max-width: 320px;
-  height: 32px;
-  padding: 0 10px 0 12px;
-  border: 0;
-  border-radius: 16px;
-  corner-shape: round;
-  background: var(--dsw-alias-bg-module-platform);
-  color: var(--dsw-alias-label-primary);
-  font: inherit;
-  font-size: 13px;
-  cursor: pointer;
-}
-
-.switcher:hover {
-  background: var(--dsw-alias-interactive-bg-hover);
-}
-
-.switcher:focus-visible,
-.linkButton:focus-visible,
-.textButton:focus-visible,
-.addButton:focus-visible,
-.switch:focus-visible,
 .iconButton:focus-visible {
   outline: 2px solid var(--dsw-alias-brand-primary);
   outline-offset: 1px;
 }
 
-.switcherLabel {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
 
 .chevron {
   flex: none;
@@ -225,86 +194,288 @@
 }
 
 /* The pill the Agent preset section marks its cards with. */
-.tag {
+.cardEnd {
   display: inline-flex;
+  flex: none;
   align-items: center;
-  border-radius: 999px;
-  corner-shape: round;
-  padding: 1px 8px;
-  background: var(--dsw-alias-bg-layer-1);
-  color: var(--dsw-alias-label-secondary);
-  font-size: 11px;
-  line-height: 17px;
-  font-weight: 500;
-  white-space: nowrap;
+  gap: 8px;
 }
 
-.tag[data-kind='builtin'] {
+.iconButton {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  border: 0;
+  border-radius: 8px;
   background: transparent;
-  border: 0.5px solid var(--dsw-alias-border-l4);
   color: var(--dsw-alias-label-tertiary);
+  cursor: pointer;
 }
 
-.tag[data-kind='off'] {
-  color: var(--dsw-alias-label-tertiary);
+.iconButton:hover:not(:disabled) {
+  background: var(--dsw-alias-interactive-bg-hover);
+  color: var(--dsw-alias-label-primary);
 }
 
-.tag[data-kind='restart'] {
-  background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 14%, transparent);
-  color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary));
+.iconButton:disabled {
+  opacity: 0.5;
+  cursor: default;
 }
 
-.tag[data-kind='problem'] {
-  background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);
-  color: var(--dsw-alias-state-error-primary);
+.iconWrap {
+  display: inline-flex;
 }
 
-.tag[data-kind='local'] {
-  background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent);
-  color: var(--dsw-alias-state-business-primary);
+/* The destructive text action in a card body. */
+.danger {
+  color: var(--dsw-alias-state-error-primary);
 }
 
-.cardEnd {
-  display: inline-flex;
-  flex: none;
+.actions {
+  display: flex;
   align-items: center;
   gap: 8px;
 }
 
-.switch {
-  box-sizing: border-box;
-  position: relative;
-  flex: 0 0 auto;
-  width: 36px;
-  height: 20px;
-  padding: 2px;
+/* The delete of a row the person added: red on its own, not only on hover. */
+.deleteButton {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
   border: 0;
-  border-radius: 10px;
-  background: var(--dsw-alias-border-l3);
+  border-radius: 8px;
+  background: transparent;
+  color: var(--dsw-alias-state-error-primary);
   cursor: pointer;
 }
 
-.switchOn {
-  background: var(--dsw-alias-brand-primary);
+.deleteButton:hover:not(:disabled) {
+  background: var(--dsw-alias-interactive-bg-hover-danger, var(--dsw-alias-interactive-bg-hover));
 }
 
-.switch:disabled {
-  cursor: default;
+.deleteButton:disabled {
   opacity: 0.5;
+  cursor: default;
 }
 
-.thumb {
-  display: block;
-  width: 16px;
-  height: 16px;
-  border-radius: 50%;
-  corner-shape: round;
-  background: var(--dsw-alias-label-primary-foreground);
-  transition: transform 120ms ease;
+.deleteButton:focus-visible {
+  outline: 2px solid var(--dsw-alias-brand-primary);
+  outline-offset: 1px;
 }
 
-.switchOn .thumb {
-  transform: translateX(16px);
+.rowNote {
+  font-size: 12px;
+  color: var(--dsw-alias-label-tertiary);
+  max-width: 180px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cardBody {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  margin: 0 14px;
+  padding: 10px 0 12px;
+  border-top: 0.5px solid var(--dsw-alias-border-l2);
+}
+
+.facts {
+  display: grid;
+  grid-template-columns: max-content minmax(0, 1fr);
+  column-gap: 14px;
+  row-gap: 4px;
+  margin: 0;
+  font-size: 12.5px;
+  line-height: 18px;
+}
+
+.facts dt {
+  color: var(--dsw-alias-label-tertiary);
+}
+
+.facts dd {
+  margin: 0;
+  min-width: 0;
+  overflow-wrap: anywhere;
+  color: var(--dsw-alias-label-primary);
+}
+
+.subLabel {
+  margin: 2px 0 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--dsw-alias-label-tertiary);
+}
+
+.reason {
+  margin: 0;
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--dsw-alias-state-error-primary);
+  overflow-wrap: anywhere;
+}
+
+.parts {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.partsHead {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.partsHead .subLabel {
+  margin: 0;
+}
+
+.partsCount {
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--dsw-alias-label-secondary);
+}
+
+.partsToggle {
+  margin-left: auto;
+}
+
+.partExceptions {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.partException {
+  display: grid;
+  grid-template-columns: 7px minmax(0, 1fr) auto;
+  align-items: center;
+  column-gap: 8px;
+  row-gap: 2px;
+  padding: 6px 10px;
+  border-radius: 8px;
+  background: var(--dsw-alias-bg-layer-1);
+  font-size: 12.5px;
+  line-height: 18px;
+}
+
+.partId {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
+  font-size: 12px;
+}
+
+.partWhy {
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--dsw-alias-label-tertiary);
+}
+
+.partGroup {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.partFailure {
+  grid-column: 2 / -1;
+  margin: 0;
+  font-size: 12px;
+  color: var(--dsw-alias-state-error-primary);
+  overflow-wrap: anywhere;
+}
+
+.partsAll {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.partsFilter {
+  width: 200px;
+}
+
+.chips {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.cards {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.card {
+  min-width: 0;
+  border: 0.5px solid var(--dsw-alias-border-l4);
+  border-radius: 14px;
+  background: var(--dsw-alias-bg-layer-3);
+}
+
+.card[data-open='true'] {
+  background: var(--dsw-alias-bg-layer-2);
+  border-color: var(--dsw-alias-border-l3);
+  box-shadow: var(--dsw-elevation-panel, none);
+}
+
+.cardHead {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 12px 14px;
+}
+
+.cardMain {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.titleRow {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  min-width: 0;
+}
+
+.cardTitle {
+  font-size: 14px;
+  line-height: 20px;
+  font-weight: 600;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.cardDesc {
+  font-size: 12.5px;
+  line-height: 18px;
+  color: var(--dsw-alias-label-tertiary);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* The pill the Agent preset section marks its cards with. */
+.cardEnd {
+  display: inline-flex;
+  flex: none;
+  align-items: center;
+  gap: 8px;
 }
 
 .iconButton {
@@ -334,33 +505,11 @@
   display: inline-flex;
 }
 
-.textButton {
-  border: 0;
-  border-radius: 8px;
-  padding: 4px 8px;
-  background: transparent;
-  color: var(--dsw-alias-label-tertiary);
-  font: inherit;
-  font-size: 12.5px;
-  line-height: 18px;
-  cursor: pointer;
-}
-
-.textButton:hover:not(:disabled) {
-  background: var(--dsw-alias-interactive-bg-hover);
-  color: var(--dsw-alias-label-primary);
-}
-
-.textButton[data-danger='true']:hover:not(:disabled),
-.actions .textButton[data-danger='true'] {
+/* The destructive text action in a card body. */
+.danger {
   color: var(--dsw-alias-state-error-primary);
 }
 
-.textButton:disabled {
-  opacity: 0.5;
-  cursor: default;
-}
-
 .actions {
   display: flex;
   align-items: center;
@@ -395,36 +544,6 @@
   outline-offset: 1px;
 }
 
-.addButton,
-.linkButton {
-  border: 0.5px solid var(--dsw-alias-border-l3);
-  border-radius: 999px;
-  padding: 3px 12px;
-  background: transparent;
-  color: var(--dsw-alias-label-primary);
-  font: inherit;
-  font-size: 12.5px;
-  line-height: 18px;
-  cursor: pointer;
-}
-
-.linkButton {
-  border-radius: 8px;
-  padding: 3px 10px;
-  font-size: 12px;
-}
-
-.addButton:hover:not(:disabled),
-.linkButton:hover:not(:disabled) {
-  background: var(--dsw-alias-interactive-bg-hover);
-}
-
-.addButton:disabled,
-.linkButton:disabled {
-  opacity: 0.5;
-  cursor: default;
-}
-
 .rowNote {
   font-size: 12px;
   color: var(--dsw-alias-label-tertiary);
@@ -501,7 +620,7 @@
   color: var(--dsw-alias-label-secondary);
 }
 
-.partsHead .linkButton {
+.partsToggle {
   margin-left: auto;
 }
 
@@ -601,27 +720,9 @@
   color: var(--dsw-alias-label-tertiary);
 }
 
-.statusDot {
-  display: inline-block;
-  width: 7px;
-  height: 7px;
+.phase {
+  display: inline-flex;
   flex: none;
-  border-radius: 999px;
-  corner-shape: round;
-  background: var(--dsw-alias-label-tertiary);
-}
-
-.statusDot[data-phase='active'] {
-  background: var(--dsw-alias-state-success-primary);
-}
-
-.statusDot[data-phase='failed'] {
-  background: var(--dsw-alias-state-error-primary);
-}
-
-.statusDot[data-phase='loading'],
-.statusDot[data-phase='pending'] {
-  background: var(--dsw-alias-state-business-primary);
 }
 
 .installBody {

+ 45 - 67
packages/client/ui-settings-plugin-manager/src/client/PluginManagerSettingsTab.tsx

@@ -14,7 +14,8 @@ import { useEffect, useId, useState, type ReactNode } from 'react'
 import clsx from 'clsx'
 import type { PluginInstallRejection, PluginPackageView, PluginRowTarget } from '@deepseek-ai/dsh-api-remotes/client'
 import {
-  Button, IconChevronDownOutline14, IconRefreshOutline16, Menu, Modal, type MenuItem,
+  Button, IconChevronDownOutline14, IconRefreshOutline16, Input, Menu, Modal, StateDot, Switch, Tag,
+  type MenuItem, type StateDotState,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type { PluginManagerLocaleKey } from './locales.ts'
@@ -58,13 +59,23 @@ const STATUS_KEYS = {
 } satisfies Record<CardStatus, PluginManagerLocaleKey>
 
 /** Status dot naming a live root-fiber phase. */
+/** Pending and unloading fibers do nothing; only loading is in progress. */
+const PHASE_STATES = {
+  pending: 'idle',
+  loading: 'ongoing',
+  active: 'done',
+  failed: 'error',
+  unloading: 'idle',
+} satisfies Record<RowPhase, StateDotState>
+
+/** One fiber phase as a state dot, named for assistive technology by the phase's copy. */
 function PhaseDot({ phase, t }: { readonly phase: RowPhase; readonly t: Translate }): ReactNode {
   const label = t(PHASE_KEYS[phase])
-  return <span className={css.statusDot} data-phase={phase} role="img" aria-label={label} title={label} />
-}
-
-function Tag({ kind, children }: { readonly kind: string; readonly children: ReactNode }): ReactNode {
-  return <span className={css.tag} data-kind={kind}>{children}</span>
+  return (
+    <span className={css.phase} role="img" aria-label={label} title={label}>
+      <StateDot state={PHASE_STATES[phase]} size={8} />
+    </span>
+  )
 }
 
 /** A component with a recorded startup failure or a failed fiber. */
@@ -122,14 +133,15 @@ function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly
         {rest.length === 0
           ? null
           : (
-            <button
-              type="button"
-              className={css.linkButton}
+            <Button
+              variant="ghost"
+              size="sm"
+              className={css.partsToggle}
               aria-expanded={open}
               onClick={() => { setOpen(current => !current) }}
             >
               {t(open ? 'partsCollapse' : 'partsShowAll')}
-            </button>
+            </Button>
           )}
       </div>
       {rows.length === 0 ? <p className={css.status}>{t('partsEmpty')}</p> : null}
@@ -139,9 +151,9 @@ function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly
           <ul className={css.partExceptions}>
             {failed.map(row => (
               <li key={row.entryId} className={css.partException} data-plugin-row={row.entryId}>
-                <span className={css.statusDot} data-phase="failed" aria-hidden="true" />
+                <StateDot state="error" size={8} />
                 <span className={css.partId}>{row.rowId}</span>
-                <Tag kind="problem">{t('rowStateFailed')}</Tag>
+                <Tag tone="danger">{t('rowStateFailed')}</Tag>
                 {row.failure === undefined ? null : <p className={css.partFailure}>{row.failure.message}</p>}
               </li>
             ))}
@@ -158,7 +170,7 @@ function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly
       {open
         ? (
           <div className={css.partsAll}>
-            <input
+            <Input
               type="search"
               className={css.partsFilter}
               placeholder={t('partsFilter')}
@@ -180,51 +192,6 @@ function BundleParts({ rows, t }: { readonly rows: readonly RowView[]; readonly
   )
 }
 
-function Switch({ checked, label, disabled, title, onChange }: {
-  readonly checked: boolean
-  readonly label: string
-  readonly disabled: boolean
-  readonly title?: string
-  readonly onChange: (checked: boolean) => void
-}): ReactNode {
-  return (
-    <button
-      type="button"
-      role="switch"
-      aria-checked={checked}
-      aria-label={label}
-      title={title}
-      className={clsx(css.switch, checked && css.switchOn)}
-      disabled={disabled}
-      onClick={() => { onChange(!checked) }}
-    >
-      <span className={css.thumb} />
-    </button>
-  )
-}
-
-/** A text action: grey in a card head, red for a destructive one. */
-function TextButton({ label, disabled, danger, onClick, children }: {
-  readonly label: string
-  readonly disabled: boolean
-  readonly danger?: boolean
-  readonly onClick: () => void
-  readonly children: ReactNode
-}): ReactNode {
-  return (
-    <button
-      type="button"
-      className={css.textButton}
-      data-danger={danger === true ? 'true' : undefined}
-      aria-label={label}
-      disabled={disabled}
-      onClick={onClick}
-    >
-      {children}
-    </button>
-  )
-}
-
 /** One **Add to…** choice, carried through the menu as its item id. */
 interface AddChoice {
   readonly declaredName: string
@@ -296,8 +263,8 @@ function PackageCard({
         <div className={css.cardMain}>
           <div className={css.titleRow}>
             <span className={css.cardTitle}>{title}</span>
-            {builtin ? <Tag kind="builtin">{t('builtinTag')}</Tag> : null}
-            {status === null ? null : <Tag kind={status}>{t(STATUS_KEYS[status])}</Tag>}
+            {builtin ? <Tag>{t('builtinTag')}</Tag> : null}
+            {status === null ? null : <Tag tone={status === 'restart' ? 'warning' : 'danger'}>{t(STATUS_KEYS[status])}</Tag>}
           </div>
           {pkg.description === undefined ? null : <span className={css.cardDesc}>{pkg.description}</span>}
         </div>
@@ -329,16 +296,16 @@ function PackageCard({
                 align="end"
                 portal
                 anchor={(
-                  <button
-                    type="button"
-                    className={css.addButton}
+                  <Button
+                    variant="outline"
+                    size="sm"
                     aria-haspopup="menu"
                     aria-expanded={addMenu}
                     disabled={busy}
                     onClick={() => { setAddMenu(current => !current) }}
                   >
                     {t('addTo')}
-                  </button>
+                  </Button>
                 )}
               />
             )}
@@ -368,10 +335,21 @@ function PackageCard({
               ? (
                 <div className={css.actions}>
                   {retryable
-                    ? <TextButton label={t('retryPackage')} disabled={busy} onClick={onRetry}>{t('retryPackage')}</TextButton>
+                    ? <Button variant="ghost" size="sm" disabled={busy} onClick={onRetry}>{t('retryPackage')}</Button>
                     : null}
                   {removable
-                    ? <TextButton label={t('uninstallLabel', { name: title })} disabled={busy} danger onClick={onUninstall}>{t('uninstall')}</TextButton>
+                    ? (
+                      <Button
+                        variant="ghost"
+                        size="sm"
+                        className={css.danger}
+                        aria-label={t('uninstallLabel', { name: title })}
+                        disabled={busy}
+                        onClick={onUninstall}
+                      >
+                        {t('uninstall')}
+                      </Button>
+                    )
                     : null}
                 </div>
               )
@@ -557,7 +535,7 @@ export function PluginManagerSettingsTab(props: PluginManagerSettingsTabProps):
         : (
           <p className={css.notice} data-kind={state.notice.kind} role={state.notice.kind === 'failed' ? 'alert' : 'status'}>
             <span>{noticeText(state.notice, t)}</span>
-            <button type="button" className={css.linkButton} onClick={props.dismissNotice}>{t('dismiss')}</button>
+            <Button variant="ghost" size="sm" onClick={props.dismissNotice}>{t('dismiss')}</Button>
           </p>
         )}
       {loaded

+ 13 - 23
packages/client/ui-settings-plugin-manager/src/client/PresetPluginsSection.tsx

@@ -8,9 +8,8 @@
  */
 
 import { useEffect, useState, type ReactNode } from 'react'
-import clsx from 'clsx'
 import type { PluginPackageView } from '@deepseek-ai/dsh-api-remotes/client'
-import { IconTrashOutline16, Menu, type MenuItem } from '@deepseek-ai/dsh-client-ui-primitives'
+import { Button, IconTrashOutline16, Menu, Switch, Tag, type MenuItem } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 // Type-only: the detail slot's declaration, owned by the roster.
 import type {} from '@deepseek-ai/dsh-client-ui-agent-preset/client'
@@ -48,10 +47,6 @@ function addChoices(packages: readonly PluginPackageView[], preset: PresetGroup,
   return items.length === 0 ? [{ id: 'none', label: t('capabilitiesAddEmpty'), disabled: true }] : items
 }
 
-function Tag({ kind, children }: { readonly kind: string; readonly children: ReactNode }): ReactNode {
-  return <span className={css.tag} data-kind={kind}>{children}</span>
-}
-
 /** One row of the preset's composition, as a card with its switch. */
 function PresetCard({ preset, row, packages, t, busy, onSetDisabled, onRemove }: {
   readonly preset: PresetGroup
@@ -71,8 +66,8 @@ function PresetCard({ preset, row, packages, t, busy, onSetDisabled, onRemove }:
         <div className={css.cardMain}>
           <div className={css.titleRow}>
             <span className={css.cardTitle}>{copy.title}</span>
-            {copy.local ? <Tag kind="local">{t('localTag')}</Tag> : null}
-            {row.fiberPhase === 'failed' ? <Tag kind="problem">{t('rowStateFailed')}</Tag> : null}
+            {copy.local ? <Tag tone="info">{t('localTag')}</Tag> : null}
+            {row.fiberPhase === 'failed' ? <Tag tone="danger">{t('rowStateFailed')}</Tag> : null}
           </div>
           {copy.description === undefined ? null : <span className={css.cardDesc}>{copy.description}</span>}
         </div>
@@ -95,18 +90,13 @@ function PresetCard({ preset, row, packages, t, busy, onSetDisabled, onRemove }:
                     </button>
                   )
                   : null}
-                <button
-                  type="button"
-                  role="switch"
-                  aria-checked={row.enabled !== false}
-                  aria-label={t('rowToggle', { name: copy.title })}
+                <Switch
+                  checked={row.enabled !== false}
+                  label={t('rowToggle', { name: copy.title })}
                   {...lockedOff ? { title: t('rowLockedByComposition') } : {}}
-                  className={clsx(css.switch, row.enabled !== false && css.switchOn)}
                   disabled={busy || preset.broken !== undefined || lockedOff}
-                  onClick={() => { onSetDisabled(id, row.enabled !== false) }}
-                >
-                  <span className={css.thumb} />
-                </button>
+                  onChange={() => { onSetDisabled(id, row.enabled !== false) }}
+                />
               </>
             )}
         </div>
@@ -144,9 +134,9 @@ export function PresetPluginsSection(props: PresetPluginsSectionProps): ReactNod
               align="end"
               portal
               anchor={(
-                <button
-                  type="button"
-                  className={css.addButton}
+                <Button
+                  variant="outline"
+                  size="sm"
                   aria-haspopup="menu"
                   aria-expanded={addMenu}
                   aria-label={t('capabilitiesAddLabel', { name: presetName })}
@@ -154,7 +144,7 @@ export function PresetPluginsSection(props: PresetPluginsSectionProps): ReactNod
                   onClick={() => { setAddMenu(current => !current) }}
                 >
                   {t('capabilitiesAdd')}
-                </button>
+                </Button>
               )}
             />
           )}
@@ -167,7 +157,7 @@ export function PresetPluginsSection(props: PresetPluginsSectionProps): ReactNod
         : (
           <p className={css.notice} data-kind={state.notice.kind} role={state.notice.kind === 'failed' ? 'alert' : 'status'}>
             <span>{noticeText(state.notice, t)}</span>
-            <button type="button" className={css.linkButton} onClick={props.dismissNotice}>{t('dismiss')}</button>
+            <Button variant="ghost" size="sm" onClick={props.dismissNotice}>{t('dismiss')}</Button>
           </p>
         )}
       {!loaded || preset !== undefined ? null : <p className={css.empty}>{t('presetMissing')}</p>}

+ 1 - 1
packages/client/ui-settings-plugin-manager/tests/components.client.spec.tsx

@@ -146,7 +146,7 @@ describe('PluginManagerSettingsTab', () => {
     expect(screen.getAllByText(en.statusRestart)).toHaveLength(2)
     expect(screen.getAllByText(en.builtinTag)).toHaveLength(3)
     expect(screen.getByText('A sidebar.')).toBeTruthy()
-    expect(document.querySelectorAll('[data-kind]')).toHaveLength(6)
+    expect(document.querySelectorAll('[data-tone]')).toHaveLength(6)
 
     const sidebar = screen.getByRole('switch', { name: 'Enable better-sidebar' }) as HTMLButtonElement
     expect(sidebar.getAttribute('aria-checked')).toBe('true')

+ 1 - 1
packages/client/ui-settings-plugin-manager/tests/preset-section.client.spec.tsx

@@ -137,7 +137,7 @@ describe('PresetPluginsSection', () => {
     expect(screen.getByText('A tool')).toBeTruthy()
     expect(screen.getAllByText(en.localTag)).toHaveLength(5)
     expect(screen.getByText(en.rowStateFailed)).toBeTruthy()
-    expect(document.querySelectorAll('[data-preset-row] [data-kind]')).toHaveLength(6)
+    expect(document.querySelectorAll('[data-preset-row] [data-tone]')).toHaveLength(6)
     expect(screen.getByText(en.rowNoId)).toBeTruthy()
     expect(screen.queryByRole('switch', { name: `Enable ${en['name.tool-todo']}` })).toBeNull()
     expect(screen.getByRole('switch', { name: `Enable ${en['name.tool-pwsh']}` }).getAttribute('aria-checked')).toBe('true')

+ 1 - 1
packages/host/plugin-manager/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-host-plugin-manager",
   "description": "Plugin management over the booted profile: install, enable, disable, and retry bundles, add rows to the global or a preset's user layer, and report each package's state through the plugins Remote",
-  "version": "0.1.2-rc.1",
+  "version": "0.1.3-alpha.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/preset/global-tool-mask/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-global-tool-mask",
   "description": "Composition-authored global-tool mask for one agent preset: a row that hides named host tools from the sessions the preset composes",
-  "version": "0.1.2-rc.1",
+  "version": "0.1.3-alpha.1",
   "publishConfig": {
     "access": "public"
   },

+ 1 - 1
packages/util/patch-file/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@deepseek-ai/dsh-patch-file",
   "description": "Loader patch-list files (cordis.patch.yml): the shared parser with the !!js dialect and relative-name anchoring, and a comment-preserving key-level writer with lock, atomic replace, and readback",
-  "version": "0.1.2-rc.1",
+  "version": "0.1.3-alpha.1",
   "publishConfig": {
     "access": "public"
   },

Some files were not shown because too many files changed in this diff