feat(meta): colourise the plugin artwork and say what it actually is

Two things the Plugins list made obvious once the icon rendered beside the
built-ins.

The artwork was flat: one blue panel, one darker blue rail. The built-ins
each carry their own two-hue palette — the file header says so outright
("these glyphs carry their own brand colors and gradients instead of riding
currentColor") — and at the 30px a row renders at, a single-hue mark reads as
a plain block. Three candidates were rendered next to the real built-in
artwork at the real sizes (30 / 36 / 72, light and dark) before picking one: a
violet rail sat too close to the panel's deep blue to read as a second colour,
and a pale teal panel washed out. The shipped pair is blue for the panel
(gradient) and teal-green for the rail (gradient), which stays legible at row
size and matches the Subagent artwork's proven blue/green combination without
sharing its silhouette.

The description sold the plugin as someone else's sidebar. It now leads with
what it is — better — in one sentence rather than a colon and a feature list,
in both locales, and both `package.json` (the npm blurb and the list's
fallback label) and `dsh.plugin.json` say the same thing. The README never
framed it that way, so nothing there changed.

The artwork guard gained the two properties this decision rests on: the mark
must be colourised (two gradients, no currentColor) and must not use a
CSS-only paint, since the icon ships as an <img> and the search artwork's
foreignObject technique would stay empty there. Red/green verified — a flat
single-hue redraw fails it.

Verified on the live 3080 instance: the row renders the new artwork and
"更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。"
with no VSCode framing anywhere in the copy.
This commit is contained in:
Menghuan1918
2026-09-23 20:46:11 +08:00
parent 3ee2d40ff9
commit 2d2d78f3f2
7 changed files with 28 additions and 12 deletions
+1 -1
View File
@@ -70,7 +70,7 @@
8. **插件开发树的 dsh-* 传递 peer 需提升为 devDependencies**(alpha.3 首见):`dsh-subagent` 等 npm 包把 `dsh-attachment` 等 dsh-* 姊妹包声明为 peerDependencies(由宿主 bundle 树统一提供,宿主侧无此问题),插件仓库若只直接依赖其中一部分,其余 peer 在 pnpm 下会解析到树上残留的旧版——如 `dsh-attachment@0.1.1-rc.1` 缺 `admitPromptContent` 导出、`dsh-subagent` 产物 import 它时测试加载即崩。因此 devDependencies 需涵盖 dev 树实际触达的全部 peer(attachment / scope / session-projection / system-prompt / user-approval / util-time 等,与直接依赖同款精确钉版);**`@deepseek-ai/cordis` peer 自 alpha.3 起要求 `^4.0.2`,0.1.7 起要求 `^4.0.3`**。适配新版本时先跑 `pnpm peers check`,把新失配的传递 peer 一并提升进 devDependencies。**0.1.7 新增的同类问题**见 §3.3 第 12 条(`ui-primitives` 的第二层裸 import)。例外:**`dsh-client-locale` 的 peer/devDep 允许落后于基线**的历史已消除,rc.1 起上游恢复发版,0.1.7 起与 DSH 同 tuple。
9. **右侧栏是 DSH 原生栏,插件只提供 tab 类型**:聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`。插件的每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`,`extension` 带)+ 原生 tab 体(`sidebar.right.pane.tab` keyed 槽,key = `dsh-better-sidebar:<id>`);`editor` 类型同时认领 `dsh-resource://file/**`(**但按 §3.2 第 5 条的清单让出一部分**),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件文件树,注销即复位)。`ctx.sidebarRight.openTab/openResource/close` 只对**在屏会话**写入;跨会话用具体类上的 `openTabIn/openResourceIn/closeIn`。原生 tab 的插件侧状态在 `src/client/native/tab-adapter.tsx`。**注册必须等服务、不能等槽声明**:原生栏先声明 `sidebar.right.pane.tab` 再 `provide('sidebarRightTabs')`,真机 profile 上槽声明回调里 `ctx.get('sidebarRightTabs')` 仍是 undefined(实测 3 秒后才出现),按槽触发注册会静默什么都不注册且永不重试——用 `ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(`tests/native-surface.spec.ts` 以「槽先触发、服务后到达」的顺序守护)。**0.1.6-alpha.2 起**:`SidebarRightGuideEntry.id` 必填且同 provider 内唯一(缺它会静默清空整个承载面);同 provider 的 guide 条目是**追加**关系,`chain` 那种「替换宿主条目」的语义只存在于 `conversation.chat.turnTail` 且已被上游改成 `list`。**0.1.7 起**:`sidebar-right` 本体的契约**零破坏**——槽名无改名、`SidebarRightTabDefinition` 没有新的必填字段、只有新增的可选 `keepMounted` 与 `ISidebarRight.mounted`,`openTabIn/openResourceIn/closeIn` 在具体 class 上字节不变;`tab.visible` 语义收紧为 `active && (...)`(全局面板/后台会话时 body 不挂载);持久化校验收紧(非根 / 列轴 / 嵌套 / 3+ docked pane 的 split 一律判非法**整份丢弃**,0.1.6 的 sidebar 自己产不出这种布局);`RightbarRoot` 现在为每个保留会话渲染一个隐藏 seat(`SidebarSessionViews`,未导出、插件拿不到)。原生 tab 体宿主 `.paneBody` 是**有确定高度的块级滚动容器**(非 flex 容器),native 适配层因此给每个 tab 体包一层 `height:100%` 的列 flex 宿主(`sidebar.module.css` 的 `.nativeTabHost`),tab 组件根继续用 `flex:1`/`height:100%`。全局面板(根级 `main` keyed 槽 + `sidebar.panellist` + `ctx.layout.selectPanel/beginNavigation`、`rightbar` 改根级并新增 `rightbar.session`)插件**不接入**,只做兼容。底部工作台的中心列锚点认 `[data-slot="main.conversation"]`(并跳过 `display: contents` 祖先,同时保留 alpha.1 的 `[data-slot="conversation"]`);文件地址语法跟随 alpha.2(`fileAddressFor` 一律 session 作用域、绝对路径保留前导 `/`、`parseFileAddress` 前缀解析并去 `?`/`#`)。**rc.1 起**:宿主原生指南只在列出的条目 ≤ 4 条时渲染说明(上游 `MAX_DESCRIBED_ENTRIES = 4`;更长的列表是整列丢弃,不是截断)——插件默认贡献 4 个 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话),**与宿主的终端条目合计后可能超过 4 条**,此时说明不渲染;插件**不恢复**旧的 `nativeGuideDesc` 通用兜底句。
10. **自定义种子必须带 fork 标记对(`meta.isSeeded: true` + `inheritedEventCount`)**:dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时 `Session.ownEvents()` 含整个种子,子会话的 inbox 折重放种子里的 `agent/inbox/spliced`,**继承父会话切割时刻未领取的 inbox 输入**。宿主 `session.fork`(api-session-controller)的调用即规范形态;回归由 `tests/sidechat-seed-validation.spec.ts`(真实 `Session.create` + 对 `ownEvents()` 跑 inbox 折)守护。**0.1.7 复查:`Session.create` 的五个参数与函数体逐字相同**;上游新增了 `buildForkSeed(events, boundary)` 导出,与本插件手工的 `buildSidechatInheritance` 职责重叠但**收益不明确**(插件还有 dangling-tool-call 退回结构化快照的自有需求),暂不替换。
11. **`dsh.plugin.json` 从来不是 DSH 契约**:0.1.7 全树 grep 仍 **0 命中**。DSH 真正读的是 `package.json` 的 `dsh.bundle.patch` + `dsh.client`(`platform`/`inject`/`immediately`/`external`)+ `exports["./client"]`。`dsh.plugin.json` 只服务第三方 registry(社区市场)通道,`lib/client-registry.js` 同理;`tests/manifest-consistency.spec.ts` 守护的是这条通道的形状,不是 DSH 契约。**`dsh.manifestVersion` 也仍是「只声明、零读」**(0.1.7 的 `package-manifest` README 自己写明「Current installers and loaders do not enforce `dsh.manifestVersion` or `engines.dsh`」)。0.1.7 的官方管控面新增:`plugin-manager` 的**多 registry 回退**(`registryPlan()`:配置源 → `fallbackRegistries`(默认含 `registry.npmmirror.com`),`network`/`timeout`/`not-found`/`no-matching-version` 会自动问下一个源;`registry` 入参只存在于 Web 对话框与 `plugin_manager` 工具,**CLI 没有 registry flag**)、`waitForInstall`、以及**插件展示元数据**:`package.json#icon`(36×36、扁平填充、蓝色渐变,与 DSH 自带图标同风格)+ `locale/<lang>.json` 的 `meta.title`/`description`。本插件**已采纳**(v0.21.0-alpha.1):`icon.svg` + `locale/en.json` + `locale/zh.json`,并由 `tests/plugin-meta.spec.ts` 守护「声明了就必须发布出去」。三条硬规则:icon 必须是**包内相对路径**且写进 `files`;`locale/en.json` 是发现基线(缺它整个目录都不读);`exports` 必须暴露 `./locale/*.json` 与 `./package.json`。**所有失败都是静默的**——元数据有问题只会退化成一行纯文字,没有任何报错。`dsh.bundle.patch` 现在多了形状校验(非法值会 `throw`,引导期跳过该 bundle),本插件是合法的单字符串。
11. **`dsh.plugin.json` 从来不是 DSH 契约**:0.1.7 全树 grep 仍 **0 命中**。DSH 真正读的是 `package.json` 的 `dsh.bundle.patch` + `dsh.client`(`platform`/`inject`/`immediately`/`external`)+ `exports["./client"]`。`dsh.plugin.json` 只服务第三方 registry(社区市场)通道,`lib/client-registry.js` 同理;`tests/manifest-consistency.spec.ts` 守护的是这条通道的形状,不是 DSH 契约。**`dsh.manifestVersion` 也仍是「只声明、零读」**(0.1.7 的 `package-manifest` README 自己写明「Current installers and loaders do not enforce `dsh.manifestVersion` or `engines.dsh`」)。0.1.7 的官方管控面新增:`plugin-manager` 的**多 registry 回退**(`registryPlan()`:配置源 → `fallbackRegistries`(默认含 `registry.npmmirror.com`),`network`/`timeout`/`not-found`/`no-matching-version` 会自动问下一个源;`registry` 入参只存在于 Web 对话框与 `plugin_manager` 工具,**CLI 没有 registry flag**)、`waitForInstall`、以及**插件展示元数据**:`package.json#icon`(36×36、纯填充无描边、**双色渐变**——与 DSH 自带的 `PluginArtwork*` 同风格:它们「carry their own brand colors instead of riding currentColor」,且每个都是多色,单色版在 30px 行内尺寸下会读成一块纯色板)+ `locale/<lang>.json` 的 `meta.title`/`description`。本插件**已采纳**(v0.21.0-alpha.1):`icon.svg` + `locale/en.json` + `locale/zh.json`,并由 `tests/plugin-meta.spec.ts` 守护「声明了就必须发布出去」。三条硬规则:icon 必须是**包内相对路径**且写进 `files`;`locale/en.json` 是发现基线(缺它整个目录都不读);`exports` 必须暴露 `./locale/*.json` 与 `./package.json`。**所有失败都是静默的**——元数据有问题只会退化成一行纯文字,没有任何报错。`dsh.bundle.patch` 现在多了形状校验(非法值会 `throw`,引导期跳过该 bundle),本插件是合法的单字符串。
---
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "dsh-external/dsh-better-sidebar",
"version": "0.21.0-alpha.1",
"main": "./lib/index.js",
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 文件变动 / 任务管理 / 侧边对话,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
"description": "更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。暴露服务供其他插件注册侧边栏页面与文件预览器",
"engines": {
"dsh": ">=0.1.7-alpha.1"
},
+9 -5
View File
@@ -1,10 +1,14 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6.5" y="9" width="23" height="18" rx="2.5" fill="#7CB7FF" />
<rect x="21.5" y="9" width="8" height="18" rx="2.5" fill="url(#a)" />
<rect x="6.5" y="9" width="23" height="18" rx="2.5" fill="url(#a)" />
<rect x="21.5" y="9" width="8" height="18" rx="2.5" fill="url(#b)" />
<defs>
<linearGradient id="a" x1="25.5" y1="9" x2="25.5" y2="27" gradientUnits="userSpaceOnUse">
<stop stop-color="#3F77D8" />
<stop offset="1" stop-color="#145AF3" />
<linearGradient id="a" x1="6.5" y1="9" x2="29.5" y2="27" gradientUnits="userSpaceOnUse">
<stop stop-color="#69B9FF" />
<stop offset="1" stop-color="#324DE2" />
</linearGradient>
<linearGradient id="b" x1="25.5" y1="9" x2="25.5" y2="27" gradientUnits="userSpaceOnUse">
<stop stop-color="#45E7A4" />
<stop offset="1" stop-color="#05909D" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 465 B

After

Width:  |  Height:  |  Size: 663 B

+1 -1
View File
@@ -1,6 +1,6 @@
{
"meta": {
"title": "Better Sidebar",
"description": "A VSCode-like right sidebar: file tree, editor, file changes, subagent tasks and side chat, isolated per conversation."
"description": "A better right sidebar — file tree, editor, changes, tasks and side chat in one column, separate for every conversation."
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"meta": {
"title": "Better Sidebar",
"description": "VSCode 风格右侧侧边栏:文件树、编辑器、文件变动、子代理任务与侧边对话,按会话隔离。"
"description": "更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-better-sidebar",
"version": "0.21.0-alpha.1",
"description": "DSH web plugin: a VSCode-like right sidebar (file tree / editor / changes / tasks / side chat), isolated per conversation. Exposes a service for other plugins to register sidebar tabs and file viewers.",
"description": "Better Sidebar for DSH — file tree, editor, changes, tasks and side chat in one column, separate for every conversation. Also exposes a service other plugins use to register sidebar tabs and file viewers.",
"type": "module",
"repository": {
"type": "git",
+14 -2
View File
@@ -91,8 +91,9 @@ describe('DSH plugin presentation metadata', () => {
it('keeps the icon in the built-in house style', () => {
const svg = readFileSync(iconPath(), 'utf8')
// Every icon DSH ships for this feature is a 36x36, fill-only geometric
// mark (see packages/experimental/*/icon.svg). Pinning the frame keeps a
// later redraw from silently changing how the mark scales in the list.
// mark (see packages/experimental/*/icon.svg and the artwork in
// ui-primitives/src/plugin-artwork.tsx). Pinning the frame keeps a later
// redraw from silently changing how the mark scales in the list.
expect(svg).toMatch(/viewBox="0 0 36 36"/u)
expect(svg).toMatch(/width="36"/u)
expect(svg).toMatch(/height="36"/u)
@@ -101,6 +102,17 @@ describe('DSH plugin presentation metadata', () => {
// a <text> node would also render in whatever font the browser picks.
expect(svg, 'no stroked outlines in the built-in style').not.toMatch(/stroke=/u)
expect(svg, 'no <text> in an icon').not.toMatch(/<text[\s>]/u)
// The built-ins "carry their own brand colors and gradients instead of
// riding currentColor" (their file header), and each is genuinely
// multi-hue — a flat single-colour redraw reads as a plain block beside
// them at the 30px row size.
expect(svg, 'the mark must be colourised, not flat').toMatch(/<linearGradient/u)
expect((svg.match(/<linearGradient/gu) ?? []).length, 'at least two gradients = two hues').toBeGreaterThanOrEqual(2)
expect(svg, 'no currentColor: plugin artwork owns its palette').not.toMatch(/currentColor/u)
// A CSS-only paint (conic-gradient via foreignObject, as the search
// artwork uses) renders inline but stays EMPTY inside the `<img>` this
// feature delivers the icon through.
expect(svg, 'no foreignObject: the icon ships as an <img>').not.toMatch(/foreignObject/u)
})
it('actually packs the icon and locales into the tarball', () => {