Sfoglia il codice sorgente

feat(web): open a row's configuration from its name and label the guide examples

- A bundle row with a configuration page is opened from its name, which carries an inline chevron like the plugin cards; the separate configure button and its `configure` label are gone. Status and switch stay at the right.
- The three install-guide examples carry an "示例:" / "Example: " label before the code.
- README and Agent Note record that a bundle's pages live with the row that carries its browser half: `dsh-client-modules` attaches that half to the row named by the bare package name, so a page keyed to a subpath row disappears with the root row.
Yichen Jiang 2 settimane fa
parent
commit
62c66dad02

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.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-16-plugin-configuration-on-the-plugins-page.md
-2026-09-16-plugin-configuration-on-the-plugins-page.md: 99694a278c7a443dd194c3d749b065ad5766b38b
-2026-09-16-plugin-configuration-on-the-plugins-page.zh.md: 29dc678024c0292ed6837c67e8fdcc09763d67be
+2026-09-16-plugin-configuration-on-the-plugins-page.md: fb330bee44373d20c7aeea8cee521bf8def025d4
+2026-09-16-plugin-configuration-on-the-plugins-page.zh.md: 65c1f5a8bf4654b395ab4bfcf8015ae7c23a6acc

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.md

@@ -22,7 +22,7 @@ A plugin's settings lived in Settings, on the Plugins section's configuration ta
 
 ## Consequences
 
-- A bundle's browser half registers a form with one slot registration and its own dictionary; the bundle's patch must declare the row under the id in the key, and the registration exists while the bundle is on.
+- A bundle's browser half registers a form with one slot registration and its own dictionary; the bundle's patch must declare the row under the id in the key, and the registration exists while the row that carries the bundle's browser half is on: `dsh-client-modules` attaches that half to the row whose specifier is the bare package name, so a row-level page keyed to a subpath row disappears with the root row, not with its own.
 - The four pages, their forms, and the settings write path are unchanged; `ui-settings-plugins` keeps its name for the section it still owns while its pages live on the Plugins page.
 - Settings lists the inventory only; the settings goldens that carried the Plugins nav entry and the configuration tab were re-recorded.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-16-plugin-configuration-on-the-plugins-page.zh.md

@@ -22,7 +22,7 @@
 
 ## 后果
 
-- 组合包的浏览器半侧用一次 slot 注册加自己的词典就能提供表单;组合包的 patch 必须以键里的 id 声明这一行,注册只在组合包开启期间存在。
+- 组合包的浏览器半侧用一次 slot 注册加自己的词典就能提供表单;组合包的 patch 必须以键里的 id 声明这一行,注册只在承载组合包浏览器半侧的那一行开启期间存在:`dsh-client-modules` 把该半侧挂在说明符恰为包名的那一行上,因此键指向子路径行的行级页面随根行消失,而不随它自己的行。
 - 四个页面、它们的表单与 settings 写入路径不变;`ui-settings-plugins` 保留其名字,因为它仍拥有那个分区,尽管它的页面住在插件页上。
 - 设置只列出清单;带有插件导航项与配置标签页的设置类 golden 已重录。
 

+ 2 - 2
packages/client/ui-plugin-manager/README.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 packages/client/ui-plugin-manager/README.md
-README.md: 04139101d2745aad1e82f3cbd7e507157b614177
-README.zh.md: 0f4207895b235c441b9087440bbcfaefe6477a24
+README.md: 5592a5c566ca058535f05096a89e75d2bf4b4e48
+README.zh.md: 2f7344c754ab6445f172c07b375c57dee372283e

+ 1 - 1
packages/client/ui-plugin-manager/README.md

@@ -77,7 +77,7 @@ The browser plugin registers the `plugins` sidebar entry and its `main` panel th
 
 ### Configuration slots
 
-The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. Which page is open is page-local state: the cards, a bundle, an official plugin, or a row of a bundle.
+The page's `main` registration declares `plugins.item`, `plugins.bundle.config`, and `plugins.row.config` as its children, so the slots exist while the page does and a registrant's `ctx.slots.inject` waits for them. `configLedgerSource` projects the three ledgers into one observable — the official items in ledger order with their labels resolved in the active locale, and the bundle and row keys — cached until a ledger or the locale moves; the page binds it as `useConfigLedger` beside the store and never names a configurable plugin itself. Which page is open is page-local state: the cards, a bundle, an official plugin, or a row of a bundle. A registration lives with the browser half that made it. `dsh-client-modules` attaches a package's browser half to the Loader row whose specifier is the bare package name, so every page a bundle registers, for itself or for any of its rows, goes away when that row is switched off; a sub-plugin whose page must outlive the other rows ships as its own package.
 
 </details>
 

+ 1 - 1
packages/client/ui-plugin-manager/README.zh.md

@@ -77,7 +77,7 @@ ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
 
 ### 配置 slot
 
-页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。打开的是哪一页是页面本地状态:卡片、某个组合包、某个官方插件,或组合包的某一行。
+页面的 `main` 注册把 `plugins.item`、`plugins.bundle.config` 与 `plugins.row.config` 声明为子 slot,因此它们与页面同生,注册方的 `ctx.slots.inject` 会等到它们出现。`configLedgerSource` 把三份账本投影成一个可观察对象——按账本顺序排列、标签按当前语言解析的官方条目,以及组合包与行的键——在账本或语言变化前保持缓存;页面把它作为 `useConfigLedger` 绑在 store 旁边,自身从不点名任何可配置插件。打开的是哪一页是页面本地状态:卡片、某个组合包、某个官方插件,或组合包的某一行。注册与做出它的浏览器半侧同生共死。`dsh-client-modules` 只把一个包的浏览器半侧挂在说明符恰为包名的那一行 Loader 行上,所以组合包为自己或任一行注册的页面,都会在那一行被关闭时一起消失;需要在其他行关闭时仍保留页面的子插件,应作为独立的包发布。
 
 </details>
 

+ 25 - 11
packages/client/ui-plugin-manager/src/client/PluginManagerPage.module.css

@@ -538,12 +538,19 @@
 }
 
 .guideExample {
-  font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
   font-size: 12px;
   color: var(--dsw-alias-label-secondary);
   overflow-wrap: anywhere;
 }
 
+.guideExample code {
+  font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
+}
+
+.guideExampleLabel {
+  color: var(--dsw-alias-label-tertiary);
+}
+
 /* What the check refused, under the field. */
 .inputError {
   margin: -4px 0 0;
@@ -1042,24 +1049,31 @@
   overflow-wrap: anywhere;
 }
 
-.rowConfigure {
+/* A row with a configuration page: its name opens the page, the arrow says so. */
+.rowOpen {
   display: inline-flex;
-  flex: none;
   align-items: center;
   gap: 2px;
-  padding: 2px 6px;
+  padding: 0;
   border: 0;
-  border-radius: 6px;
   background: transparent;
-  color: var(--dsw-alias-label-secondary);
-  font-size: 12.5px;
-  line-height: 18px;
-  white-space: nowrap;
+  color: inherit;
+  font: inherit;
+  text-align: left;
   cursor: pointer;
 }
 
-.rowConfigure:hover {
-  background: var(--dsw-alias-interactive-bg-hover);
+.rowOpen:hover .rowId {
+  text-decoration: underline;
+  text-underline-offset: 3px;
+}
+
+.rowOpenIcon {
+  flex: none;
+  color: var(--dsw-alias-label-tertiary);
+}
+
+.rowOpen:hover .rowOpenIcon {
   color: var(--dsw-alias-label-primary);
 }
 

+ 12 - 10
packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx

@@ -181,21 +181,20 @@ function RowsSection({ rows, t, toggle, configure }: {
                 <div className={css.rowLine}>
                   <span className={css.rowIcon} aria-hidden="true"><IconCordisPluginOutline14 /></span>
                   <div className={css.rowMain}>
-                    <span className={css.rowId}>{row.rowId}</span>
+                    {configure?.has(row) === true
+                      ? (
+                        <button type="button" className={css.rowOpen} aria-label={t('configureRow', { name: row.rowId })} onClick={() => { configure.open(row) }}>
+                          <span className={css.rowId}>{row.rowId}</span>
+                          <IconChevronRightOutline14 className={css.rowOpenIcon} aria-hidden="true" />
+                        </button>
+                      )
+                      : <span className={css.rowId}>{row.rowId}</span>}
                     <span className={css.rowModule}>{row.moduleName}</span>
                   </div>
                   <span className={css.rowState}>
                     <StateDot state={rowDotState(row)} size={8} />
                     {rowStateText(row, t)}
                   </span>
-                  {configure?.has(row) === true
-                    ? (
-                      <button type="button" className={css.rowConfigure} aria-label={t('configureRow', { name: row.rowId })} onClick={() => { configure.open(row) }}>
-                        <span>{t('configure')}</span>
-                        <IconChevronRightOutline14 aria-hidden="true" />
-                      </button>
-                    )
-                    : null}
                   {toggle === undefined
                     ? null
                     : <RowSwitch row={row} t={t} busy={toggle.busy(row)} onChange={(enabled) => { toggle.onSetEnabled(row, enabled) }} />}
@@ -633,7 +632,10 @@ function InstallDialog({ install, t, onClose, onEditSpec, onRun, onCancel, onTog
                       <span className={css.guideIndex} aria-hidden="true">{index + 1}</span>
                       <div className={css.guideMain}>
                         <span className={css.guideTitle}>{t(titleKey)}</span>
-                        <code className={css.guideExample}>{t(exampleKey)}</code>
+                        <span className={css.guideExample}>
+                          <span className={css.guideExampleLabel}>{t('installGuideExampleLabel')}</span>
+                          <code>{t(exampleKey)}</code>
+                        </span>
                         <span className={css.guideHint}>{t(hintKey)}</span>
                       </div>
                       <Button

+ 2 - 2
packages/client/ui-plugin-manager/src/client/locales.ts

@@ -47,7 +47,6 @@ export const zh = {
   backToList: '返回插件列表',
   crumbRoot: '插件列表',
   backToPackage: '返回 {name}',
-  configure: '配置',
   configureRow: '配置 {name}',
   rowStateIdle: '未运行',
   uninstall: '卸载',
@@ -69,6 +68,7 @@ export const zh = {
   installGuidePathTitle: '本地插件目录',
   installGuidePathExample: '/Users/name/my-plugin',
   installGuidePathHint: '本机上插件目录的绝对路径,适用于自行开发或已下载的插件。',
+  installGuideExampleLabel: '示例:',
   installGuideFill: '填入示例',
   installGuideFillAria: '填入示例 {example}',
   installGuideSafety: '请确认插件来源可信。插件在本机以你的权限运行,来源不明的插件可能损坏 DeepSeek Harness,或读取和泄露你的数据。',
@@ -208,7 +208,6 @@ export const en = {
   backToList: 'Back to plugins',
   crumbRoot: 'Plugins',
   backToPackage: 'Back to {name}',
-  configure: 'Configure',
   configureRow: 'Configure {name}',
   rowStateIdle: 'Not running',
   uninstall: 'Uninstall',
@@ -230,6 +229,7 @@ export const en = {
   installGuidePathTitle: 'Local plugin directory',
   installGuidePathExample: '/Users/name/my-plugin',
   installGuidePathHint: 'The absolute path of a plugin directory on this machine, developed here or downloaded.',
+  installGuideExampleLabel: 'Example: ',
   installGuideFill: 'Use example',
   installGuideFillAria: 'Use the example {example}',
   installGuideSafety: 'Install only plugins you trust: they run with your permissions and can damage DeepSeek Harness or leak your data.',