@liuzw/dsh-web-ui-custom 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 liuzwZYW
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,51 @@
1
- # Temporary Holding Version
1
+ # @liuzw/dsh-web-ui-custom
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 给 **DeepSeek Harness Web** 的两项界面定制,做成插件——升级 DSH 不会再被覆盖:
4
+
5
+ 1. **PWA 应用图标**:深色圆角底 + 白色鲸鱼,替换上游白底灰鲸那一套;
6
+ 2. **侧栏设置齿轮**:账户行右侧(收起时在头像上方)加一个齿轮,单击直接打开设置面板。
7
+
8
+ > 之前的做法是直接改 DSH 安装目录里的前端 `dist` 和客户端 bundle,每次升级都会被还原。这个包用 DSH 的插件接口重做了一遍,安装、卸载、升级互不影响。
9
+
10
+ ## 安装
11
+
12
+ **从 npm(推荐)**——在 DSH 的插件市场里搜 `@liuzw/dsh-web-ui-custom` 安装,或手动:
13
+
14
+ ```bash
15
+ cd ~/.dsh/profiles/web
16
+ pnpm add @liuzw/dsh-web-ui-custom
17
+ ```
18
+
19
+ 然后把包名加进 `package.json` 的 `dsh.profile.bundles`:
20
+
21
+ ```json
22
+ "bundles": ["...", "@liuzw/dsh-web-ui-custom"]
23
+ ```
24
+
25
+ 重启 `dsh web` 生效。
26
+
27
+ **源码方式**(本仓库 clone 后):
28
+
29
+ ```bash
30
+ ./install.sh # 写入 file: 依赖 + bundle,并 pnpm install --offline
31
+ ./uninstall.sh # 撤销
32
+ ```
33
+
34
+ 两条脚本都要求先停掉 `dsh web/serve`,失败会自动回滚 profile 配置。
35
+
36
+ ## 实现
37
+
38
+ | 部分 | 做法 |
39
+ | --- | --- |
40
+ | 图标(宿主 `index.js`) | `webServer.register` 以 **exact route** 接住 `/icon-192.png`、`/icon-512.png`、`/icon-maskable-512.png`、`/apple-touch-icon.png`,返回包内 `assets/` 的 PNG。上游 `manifest.webmanifest` 与 `index.html` 引用的正是这些路径,因此前端 `dist` 一个字节都不用改。 |
41
+ | 齿轮(浏览器 `client.js`) | 在侧栏账户行追加一个按钮;点击时沿 React fiber 向上找到启动器组件的 `openSettings` 属性并调用。React 重渲染会移除外来子节点,用 MutationObserver 补回。 |
42
+
43
+ ## 行为与边界
44
+
45
+ - 图标路由带 `cache-control: no-cache`,DSH 静态资源本身也是 `no-cache`,刷新即可看到新图标;已安装的 PWA 若仍显示旧图标,是系统缓存,卸载重装 PWA 即可。
46
+ - 齿轮依赖 `_settingsArea` / `_triggerRow` 两个 CSS Module 后缀和启动器组件上的 `openSettings`。DSH 改这两处时齿轮会静默不显示(图标不受影响),届时只需调整 `client.js` 里的 `findRow`。
47
+ - 宿主以 exact match 匹配图标路径,DSH 若改文件名,需要同步更新 `index.js` 的 `ICONS` 表。
48
+
49
+ ## 许可
50
+
51
+ MIT
Binary file
Binary file
Binary file
Binary file
package/client.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Client half of the local Web UI kit: a settings gear beside the sidebar
3
+ * account launcher.
4
+ *
5
+ * The shipped account row opens a menu; the gear is a direct shortcut into the
6
+ * settings dialog. It is mounted as a sibling of the row's anchor and marks the
7
+ * row with `data-dsh-wuc-row`, so the stylesheet below can lay the pair out
8
+ * on one line when the sidebar is wide and stack the gear above the avatar in
9
+ * the 56px rail.
10
+ *
11
+ * How the settings dialog is opened: the row is the shipped launcher, and the
12
+ * settings shell hands that component an `openSettings` prop. React keeps the
13
+ * fiber for every host element under `__reactFiber$…`, so walking up from the
14
+ * row's own node reaches the component fiber that carries the callback. This
15
+ * uses no private module import and degrades quietly: when the prop is not
16
+ * found the gear is removed instead of becoming a dead button.
17
+ *
18
+ * React re-renders replace foreign children, so a MutationObserver re-mounts
19
+ * the gear after any reconciliation that drops it.
20
+ *
21
+ * @module dsh-web-ui-custom
22
+ */
23
+ window.__ModuleLoader__.load({
24
+ id: '@liuzw/dsh-web-ui-custom',
25
+ factory() {
26
+ /** Marks the account row our gear belongs to. */
27
+ const ROW_ATTR = 'data-dsh-wuc-row'
28
+ /** Marks the gear button itself. */
29
+ const GEAR_ATTR = 'data-dsh-wuc-gear'
30
+
31
+ const LABEL = (typeof navigator !== 'undefined' && /^zh/i.test(navigator.language ?? '')) ? '设置' : 'Settings'
32
+
33
+ /** 16px outline gear, drawn with `currentColor` so it follows the row. */
34
+ const GEAR_SVG = [
35
+ '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">',
36
+ '<circle cx="12" cy="12" r="3.2"/>',
37
+ '<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
38
+ '</svg>',
39
+ ].join('')
40
+
41
+ const CSS = [
42
+ /* One line: anchor takes the space, gear keeps its box. */
43
+ `[${ROW_ATTR}]{display:flex;align-items:center;gap:4px}`,
44
+ `[${ROW_ATTR}]>:first-child{flex:1;min-width:0}`,
45
+ `[${GEAR_ATTR}]{flex:none;width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:8px;background:transparent;color:inherit;cursor:pointer}`,
46
+ `[${GEAR_ATTR}]:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}`,
47
+ `[${GEAR_ATTR}]:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4f6ef7);outline-offset:2px}`,
48
+ /* Rail: stack the gear above the avatar, keeping the icon column centred. */
49
+ `[class*="_collapsed"] [${ROW_ATTR}]{flex-direction:column;gap:8px}`,
50
+ `[class*="_collapsed"] [${GEAR_ATTR}]{order:-1}`,
51
+ ].join('')
52
+
53
+ /**
54
+ * The shipped account launcher row.
55
+ * @returns the row element, or `undefined` while the sidebar is not mounted.
56
+ */
57
+ function findRow() {
58
+ const area = document.querySelector('[class*="_settingsArea"]')
59
+ if (area === null) return undefined
60
+ const trigger = area.querySelector('[class*="_triggerRow"]')
61
+ const row = (trigger ?? area).firstElementChild
62
+ return row instanceof HTMLElement ? row : undefined
63
+ }
64
+
65
+ /**
66
+ * Walk up the React fiber chain looking for the launcher's `openSettings`.
67
+ * @param node - a host element inside the launcher's subtree.
68
+ * @returns the callback the settings shell handed the launcher, if any.
69
+ */
70
+ function resolveOpenSettings(node) {
71
+ const key = Object.keys(node).find((name) => name.startsWith('__reactFiber$'))
72
+ if (key === undefined) return undefined
73
+ let fiber = node[key]
74
+ for (let depth = 0; fiber && depth < 64; depth += 1) {
75
+ const props = fiber.memoizedProps
76
+ if (props && typeof props.openSettings === 'function') return props.openSettings
77
+ fiber = fiber.return
78
+ }
79
+ return undefined
80
+ }
81
+
82
+ /**
83
+ * Mount the gear into the account row when both the row and its settings
84
+ * callback are present.
85
+ * @param row - the launcher row.
86
+ */
87
+ function mount(row) {
88
+ if (row.querySelector(`:scope > [${GEAR_ATTR}]`) !== null) return
89
+ if (typeof resolveOpenSettings(row) !== 'function') return
90
+
91
+ row.setAttribute(ROW_ATTR, '')
92
+ const gear = document.createElement('button')
93
+ gear.type = 'button'
94
+ gear.dataset.dshWucGear = ''
95
+ gear.setAttribute('aria-label', LABEL)
96
+ gear.title = LABEL
97
+ gear.innerHTML = GEAR_SVG
98
+ gear.addEventListener('click', (event) => {
99
+ event.preventDefault()
100
+ event.stopPropagation()
101
+ const open = resolveOpenSettings(row)
102
+ if (typeof open === 'function') open()
103
+ })
104
+ row.append(gear)
105
+ }
106
+
107
+ return {
108
+ /**
109
+ * Install the stylesheet, the gear, and the observer that keeps it there.
110
+ * @param ctx - browser plugin context, for effect ownership.
111
+ */
112
+ apply(ctx) {
113
+ ctx.effect(() => {
114
+ const style = document.createElement('style')
115
+ style.dataset.plugin = '@liuzw/dsh-web-ui-custom'
116
+ style.textContent = CSS
117
+ document.head.append(style)
118
+
119
+ let scheduled = false
120
+ const sync = () => {
121
+ scheduled = false
122
+ const row = findRow()
123
+ if (row !== undefined) mount(row)
124
+ }
125
+ const schedule = () => {
126
+ if (scheduled) return
127
+ scheduled = true
128
+ requestAnimationFrame(sync)
129
+ }
130
+
131
+ sync()
132
+ const observer = new MutationObserver(schedule)
133
+ observer.observe(document.body, { childList: true, subtree: true })
134
+ return () => {
135
+ observer.disconnect()
136
+ style.remove()
137
+ for (const gear of document.querySelectorAll(`[${GEAR_ATTR}]`)) gear.remove()
138
+ for (const row of document.querySelectorAll(`[${ROW_ATTR}]`)) row.removeAttribute(ROW_ATTR)
139
+ }
140
+ }, '@liuzw/dsh-web-ui-custom: sidebar settings gear')
141
+ },
142
+ }
143
+ },
144
+ })
@@ -0,0 +1,4 @@
1
+ # Bundle patch: mount the local Web UI kit (brand PWA icons + sidebar settings gear).
2
+ - insert:
3
+ - id: web-ui-custom
4
+ name: '@liuzw/dsh-web-ui-custom'
package/index.js ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Host half of the local Web UI kit.
3
+ *
4
+ * The shipped Web frontend serves its PWA icons from its own `dist/`, so an
5
+ * upgrade replaces any replaced image files. This row claims the four icon
6
+ * paths as exact routes instead and answers with the brand set kept inside
7
+ * this package, which leaves the frontend untouched and survives upgrades.
8
+ *
9
+ * The manifest itself is not claimed here: it keeps pointing at
10
+ * `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`, and the built
11
+ * `index.html` still links `./apple-touch-icon.png`. Exact routes win over the
12
+ * static file of the same path, so both keep resolving to these bytes.
13
+ *
14
+ * @module dsh-web-ui-custom
15
+ */
16
+ import { readFileSync } from 'node:fs'
17
+ import { fileURLToPath } from 'node:url'
18
+ import { dirname, join } from 'node:path'
19
+
20
+ /** Stable Cordis plugin name. */
21
+ export const name = 'web-ui-custom'
22
+
23
+ /** The web server is required before any icon route can be claimed. */
24
+ export const inject = ['webServer']
25
+
26
+ /** Icon files served from this package, keyed by the path the frontend links. */
27
+ const ICONS = {
28
+ '/icon-192.png': 'icon-192.png',
29
+ '/icon-512.png': 'icon-512.png',
30
+ '/icon-maskable-512.png': 'icon-maskable-512.png',
31
+ '/apple-touch-icon.png': 'apple-touch-icon.png',
32
+ }
33
+
34
+ const ASSET_DIR = join(dirname(fileURLToPath(import.meta.url)), 'assets')
35
+
36
+ /**
37
+ * Read one bundled icon once.
38
+ * @param file - file name inside `assets/`.
39
+ * @returns the file bytes, or `undefined` when the package is incomplete.
40
+ */
41
+ function readIcon(file) {
42
+ try {
43
+ return readFileSync(join(ASSET_DIR, file))
44
+ } catch {
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Serve the brand icons over the shipped asset paths.
51
+ * @param ctx - plugin context carrying the webServer service.
52
+ */
53
+ export function apply(ctx) {
54
+ ctx.effect(() => {
55
+ const disposers = []
56
+ for (const [route, file] of Object.entries(ICONS)) {
57
+ const body = readIcon(file)
58
+ if (body === undefined) {
59
+ ctx.logger?.warn?.(`web-ui-custom: missing asset ${file}, keeping the shipped icon for ${route}`)
60
+ continue
61
+ }
62
+ disposers.push(ctx.webServer.register({
63
+ kind: 'exact',
64
+ path: route,
65
+ handler: (request, response) => {
66
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
67
+ response.writeHead(405, { allow: 'GET, HEAD' })
68
+ response.end()
69
+ return
70
+ }
71
+ response.writeHead(200, {
72
+ 'content-type': 'image/png',
73
+ 'content-length': body.byteLength,
74
+ /* Revalidate so a replaced icon reaches an installed app. */
75
+ 'cache-control': 'no-cache',
76
+ })
77
+ if (request.method === 'HEAD') response.end()
78
+ else response.end(body)
79
+ },
80
+ }))
81
+ }
82
+ return () => {
83
+ for (const dispose of disposers) dispose()
84
+ }
85
+ }, 'web-ui-custom: icon routes')
86
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@liuzw/dsh-web-ui-custom",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Custom PWA icons and a sidebar settings gear for the DeepSeek Harness Web app. 为 DSH Web 换上自制 PWA 图标,并在侧栏账户行加一个直开设置的齿轮。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./client": "./client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "client.js",
16
+ "cordis.patch.yml",
17
+ "assets/*.png"
18
+ ],
19
+ "keywords": [
20
+ "deepseek-harness",
21
+ "dsh",
22
+ "dsh-plugin",
23
+ "pwa",
24
+ "icons",
25
+ "sidebar",
26
+ "settings",
27
+ "web"
28
+ ],
29
+ "author": "liuzwZYW",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/liuzwZYW/dsh-web-ui-custom.git"
33
+ },
34
+ "homepage": "https://github.com/liuzwZYW/dsh-web-ui-custom#readme",
35
+ "bugs": {
36
+ "url": "https://github.com/liuzwZYW/dsh-web-ui-custom/issues"
37
+ },
38
+ "peerDependencies": {
39
+ "@deepseek-ai/dsh": ">=0.2.0 <0.3.0-0"
40
+ },
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
44
+ "dsh": {
45
+ "bundle": {
46
+ "patch": "./cordis.patch.yml"
47
+ },
48
+ "client": {
49
+ "platform": "web",
50
+ "immediately": true
51
+ }
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
6
56
  }