dsh-clean-desktop-shell 0.1.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 +21 -0
- package/README.en.md +91 -0
- package/README.md +90 -0
- package/build/icon.png +0 -0
- package/cordis.patch.yml +4 -0
- package/electron/assets/tray-16.png +0 -0
- package/electron/assets/tray-32.png +0 -0
- package/electron/config.js +48 -0
- package/electron/main.js +117 -0
- package/electron/preload.js +36 -0
- package/electron/service.js +281 -0
- package/electron/tray.js +183 -0
- package/electron/update.js +74 -0
- package/electron/window.js +69 -0
- package/lib/client.js +10 -0
- package/lib/index.js +13 -0
- package/package.json +104 -0
- package/scripts/build.mjs +12 -0
- package/scripts/gen-icons.mjs +70 -0
- package/src/client/client.js +10 -0
- package/src/host/index.js +13 -0
- package/version.txt +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Icather
|
|
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.en.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-clean-desktop-shell
|
|
4
|
+
|
|
5
|
+
**A clean desktop shell for DeepSeek Harness, shipped as a DSH plugin**
|
|
6
|
+
|
|
7
|
+
Does exactly one thing: wraps your already-configured DSH Web in a clean native desktop window — system tray, single instance, auto-launch, just like a normal app. No frosted glass, no fancy materials. **Clean.**
|
|
8
|
+
|
|
9
|
+
[English](README.en.md) · [中文](README.md)
|
|
10
|
+
|
|
11
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell/releases/latest)
|
|
14
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
## What is this
|
|
19
|
+
|
|
20
|
+
`dsh-clean-desktop-shell` is a **DSH-plugin-shaped clean desktop shell**: it wraps an already-running DSH Web (default `http://127.0.0.1:3080`) in a native desktop window — system tray, single instance, auto-launch, so it behaves like any normal desktop app. **No visual changes at all**: no frosted glass, no skinning — purely a window shell.
|
|
21
|
+
|
|
22
|
+
Key differences from other desktop clients in the ecosystem:
|
|
23
|
+
|
|
24
|
+
| | Other desktop clients (e.g. dsh-desktop family) | This plugin |
|
|
25
|
+
|:--|:--|:--|
|
|
26
|
+
| **Form** | Standalone Electron app with its own profile | **DSH plugin** mounted into your existing profile |
|
|
27
|
+
| **Profile** | New `desktop` profile, plugins/config must be reinstalled | **Reuses your web profile**, zero migration |
|
|
28
|
+
| **Visual changes** | Custom title bar / frosted glass etc. | **None** — pure window shell |
|
|
29
|
+
| **Upstream** | Pinned version | **Tracks rc.7** |
|
|
30
|
+
|
|
31
|
+
## Architecture
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌─────────────────── Core (dsh web / headless service) ───────────────────┐
|
|
35
|
+
│ Sessions · Agent · Plugins · Memory live here, decoupled from UI │
|
|
36
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
37
|
+
▲
|
|
38
|
+
┌───────────────┴───────────────┐
|
|
39
|
+
│ dsh-clean-desktop-shell │
|
|
40
|
+
│ Electron shell (client) │
|
|
41
|
+
│ tray · single-instance · │
|
|
42
|
+
│ auto-launch │
|
|
43
|
+
└────────────────────────────────┘
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Default**: loads the local `127.0.0.1:3080` (your configured web profile, zero migration).
|
|
47
|
+
- **Remote-capable**: configure any remote DSH address; the shell is just a window. Phones / Linux / other devices can reach the core via browser or PWA — the shell is never bound to a local service.
|
|
48
|
+
|
|
49
|
+
### Platform matrix
|
|
50
|
+
|
|
51
|
+
| Platform | Shell | Status |
|
|
52
|
+
|:--|:--|:--|
|
|
53
|
+
| Windows | ✅ Electron (frameless + native window buttons) | First release target |
|
|
54
|
+
| macOS | ✅ Electron (hiddenInset) | Planned |
|
|
55
|
+
| Linux | — (browser / PWA to the core) | Not planned |
|
|
56
|
+
| Termux / phone / tablet | — (headless / PWA to the core) | Covered by remote core access |
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
dsh plugin --profile web add dsh-clean-desktop-shell
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Restart `dsh web`, then launch the shell from the tray / shortcut.
|
|
65
|
+
|
|
66
|
+
> The shell needs a reachable `dsh web` service (local or configured remote address). See Usage.
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
1. Start `dsh web` (or configure a remote service address).
|
|
71
|
+
2. Launch the shell: it auto-detects local 3080; if not running it starts the service per configuration.
|
|
72
|
+
3. Drag the window by its top area (right side reserved for native buttons); close minimizes to tray by default.
|
|
73
|
+
|
|
74
|
+
## Development
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
npm install
|
|
78
|
+
npm run build # build the plugin bundle
|
|
79
|
+
npm run dev # launch the shell (dev mode)
|
|
80
|
+
npm run pack # package NSIS (Win) / DMG (mac)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Changelog
|
|
84
|
+
|
|
85
|
+
### 0.1.0
|
|
86
|
+
- Initial release: Electron shell skeleton, system tray / single instance / auto-launch, DSH plugin mounting.
|
|
87
|
+
|
|
88
|
+
## Credits
|
|
89
|
+
|
|
90
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — the core.
|
|
91
|
+
- Architecture inspired by [Hermes Agent Desktop](https://github.com/NousResearch/hermes-agent)'s shell/core separation.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# dsh-clean-desktop-shell
|
|
4
|
+
|
|
5
|
+
**DeepSeek Harness 的纯净桌面壳(DSH 插件形态)**
|
|
6
|
+
|
|
7
|
+
只做一件事:给已配置好的 DSH Web 加一层干净的桌面窗口——系统托盘、单实例、开机自启、像普通软件一样用。无毛玻璃、无花哨材质,**纯净**。
|
|
8
|
+
|
|
9
|
+
[English](README.en.md) · [中文](README.md)
|
|
10
|
+
|
|
11
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell)
|
|
12
|
+
[](LICENSE)
|
|
13
|
+
[](https://github.com/Icather/dsh-clean-desktop-shell/releases/latest)
|
|
14
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
## 这是什么
|
|
19
|
+
|
|
20
|
+
`dsh-clean-desktop-shell` 是一个 **DSH 插件形态** 的纯净桌面壳:它给已经跑起来的 DSH Web(默认 `http://127.0.0.1:3080`)套一层原生桌面窗口——系统托盘、单实例、开机自启,像普通桌面软件一样使用。**不做任何视觉改造**:不加毛玻璃、不改界面,纯粹是"窗口壳"。
|
|
21
|
+
|
|
22
|
+
与生态里其他桌面端方案的最大区别:
|
|
23
|
+
|
|
24
|
+
| | 其他桌面端(如 dsh-desktop 系列) | 本插件 |
|
|
25
|
+
|:--|:--|:--|
|
|
26
|
+
| **形态** | 独立 Electron 应用,自带独立 profile | **DSH 插件**,挂载进现有 profile |
|
|
27
|
+
| **Profile** | 新建 desktop profile,插件/配置要重装 | **复用现有 web profile**,零迁移 |
|
|
28
|
+
| **视觉改造** | 自绘标题栏 / 毛玻璃等 | **零改造**,纯净窗口壳 |
|
|
29
|
+
| **跟随上游** | 固定版本 | **跟随 rc.7** |
|
|
30
|
+
|
|
31
|
+
## 架构
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌──────────────────── 内核(dsh web / headless 服务) ────────────────────┐
|
|
35
|
+
│ 会话 · Agent · 插件 · 记忆 都在这层,与界面解耦 │
|
|
36
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
37
|
+
▲
|
|
38
|
+
┌───────────────┴───────────────┐
|
|
39
|
+
│ dsh-clean-desktop-shell │
|
|
40
|
+
│ Electron 壳(客户端) │
|
|
41
|
+
│ 托盘 · 单实例 · 开机自启 │
|
|
42
|
+
└────────────────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **默认**:加载本地 `127.0.0.1:3080`(已配置好的 web profile,零迁移)。
|
|
46
|
+
- **可配远程**:在设置里填入任意远程 DSH 地址,壳只当窗口——手机 / Linux / 其他设备通过浏览器或 PWA 也能接入内核,壳本身不绑定本地服务。
|
|
47
|
+
|
|
48
|
+
### 平台矩阵
|
|
49
|
+
|
|
50
|
+
| 平台 | 壳 | 状态 |
|
|
51
|
+
|:--|:--|:--|
|
|
52
|
+
| Windows | ✅ Electron(无边框 + 原生窗口按钮) | 首发目标 |
|
|
53
|
+
| macOS | ✅ Electron(hiddenInset) | 规划中 |
|
|
54
|
+
| Linux | —(浏览器 / PWA 直连内核) | 不做 |
|
|
55
|
+
| Termux / 手机 / 平板 | —(headless / PWA 直连内核) | 由内核远程访问支持 |
|
|
56
|
+
|
|
57
|
+
## 安装
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
dsh plugin --profile web add dsh-clean-desktop-shell
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
重启 `dsh web` 后,从系统托盘/快捷方式启动桌面壳即可。
|
|
64
|
+
|
|
65
|
+
> 桌面壳需要本机有可用的 `dsh web` 服务(或配置的远程地址)。见下方「使用」。
|
|
66
|
+
|
|
67
|
+
## 使用
|
|
68
|
+
|
|
69
|
+
1. 启动 `dsh web`(或配置远程服务地址)。
|
|
70
|
+
2. 启动桌面壳:自动检测本地 3080;未运行时按配置拉起服务。
|
|
71
|
+
3. 窗口顶部(右侧留系统按钮)可拖动窗口;关闭默认缩到托盘。
|
|
72
|
+
|
|
73
|
+
## 开发
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npm install
|
|
77
|
+
npm run build # 构建插件 bundle
|
|
78
|
+
npm run dev # 启动壳(开发模式)
|
|
79
|
+
npm run pack # 打包 NSIS (Win) / DMG (mac)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 更新历史
|
|
83
|
+
|
|
84
|
+
### 0.1.0
|
|
85
|
+
- 初始版本:Electron 壳骨架,系统托盘/单实例/开机自启,DSH 插件挂载。
|
|
86
|
+
|
|
87
|
+
## 致谢
|
|
88
|
+
|
|
89
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) —— 内核本体。
|
|
90
|
+
- 架构参考 [Hermes Agent Desktop](https://github.com/NousResearch/hermes-agent) 的壳/内核分离设计。
|
package/build/icon.png
ADDED
|
Binary file
|
package/cordis.patch.yml
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell config persistence (userData/config.json).
|
|
3
|
+
*
|
|
4
|
+
* Fields:
|
|
5
|
+
* - targetUrl: default 'http://127.0.0.1:3080' — set to any remote
|
|
6
|
+
* DSH address to run the shell as a pure window.
|
|
7
|
+
* - autoStartService: default true — only applies to the local default URL.
|
|
8
|
+
* - closeToTray: default true.
|
|
9
|
+
* - windowMode: 'advanced' (mica/vibrancy) | 'compatibility'.
|
|
10
|
+
* - autoLaunch: open at login.
|
|
11
|
+
*/
|
|
12
|
+
import { app } from 'electron'
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
const DEFAULTS = {
|
|
17
|
+
targetUrl: 'http://127.0.0.1:3080',
|
|
18
|
+
autoStartService: true,
|
|
19
|
+
closeToTray: true,
|
|
20
|
+
windowMode: 'advanced',
|
|
21
|
+
autoLaunch: false,
|
|
22
|
+
backendPath: null,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let cached = null
|
|
26
|
+
|
|
27
|
+
function configPath() {
|
|
28
|
+
return join(app.getPath('userData'), 'config.json')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
if (cached) return cached
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(configPath(), 'utf8')
|
|
35
|
+
cached = { ...DEFAULTS, ...JSON.parse(raw) }
|
|
36
|
+
} catch {
|
|
37
|
+
cached = { ...DEFAULTS }
|
|
38
|
+
}
|
|
39
|
+
return cached
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function saveConfig(next) {
|
|
43
|
+
cached = { ...DEFAULTS, ...next }
|
|
44
|
+
const file = configPath()
|
|
45
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
46
|
+
writeFileSync(file, JSON.stringify(cached, null, 2), 'utf8')
|
|
47
|
+
return cached
|
|
48
|
+
}
|
package/electron/main.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — Electron main process entry.
|
|
3
|
+
*
|
|
4
|
+
* Shell/core decoupling:
|
|
5
|
+
* - default target: http://127.0.0.1:3080 (existing web profile, zero migration)
|
|
6
|
+
* - configurable remote target (settings file)
|
|
7
|
+
* - local backend auto-start on launch; full manual control from the tray
|
|
8
|
+
*
|
|
9
|
+
* The main window is a pure shell — all backend controls live in the tray.
|
|
10
|
+
*/
|
|
11
|
+
import { app, BrowserWindow } from 'electron'
|
|
12
|
+
import { createMainWindow } from './window.js'
|
|
13
|
+
import { createTray, refreshTrayMenu } from './tray.js'
|
|
14
|
+
import { loadConfig, saveConfig } from './config.js'
|
|
15
|
+
import { detect, start } from './service.js'
|
|
16
|
+
|
|
17
|
+
const isMac = process.platform === 'darwin'
|
|
18
|
+
|
|
19
|
+
// Windows: pin the AppUserModelId so the taskbar shows our whale icon
|
|
20
|
+
// instead of the generic Electron icon.
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
app.setAppUserModelId('com.icather.dsh-clean-desktop-shell')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Single instance: a second launch just focuses the existing window. */
|
|
26
|
+
const gotLock = app.requestSingleInstanceLock()
|
|
27
|
+
if (!gotLock) {
|
|
28
|
+
app.quit()
|
|
29
|
+
} else {
|
|
30
|
+
let mainWindow = null
|
|
31
|
+
let tray = null
|
|
32
|
+
|
|
33
|
+
app.on('second-instance', () => {
|
|
34
|
+
if (mainWindow) {
|
|
35
|
+
if (mainWindow.isMinimized()) mainWindow.restore()
|
|
36
|
+
mainWindow.focus()
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
async function createWindow() {
|
|
41
|
+
const config = loadConfig()
|
|
42
|
+
const target = config.targetUrl || 'http://127.0.0.1:3080'
|
|
43
|
+
|
|
44
|
+
mainWindow = createMainWindow({ target })
|
|
45
|
+
mainWindow.on('closed', () => {
|
|
46
|
+
mainWindow = null
|
|
47
|
+
})
|
|
48
|
+
mainWindow.on('close', (event) => {
|
|
49
|
+
// Close hides to tray unless we are actually quitting.
|
|
50
|
+
if (!app.isQuitting && config.closeToTray !== false) {
|
|
51
|
+
event.preventDefault()
|
|
52
|
+
mainWindow?.hide()
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return mainWindow
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Launch-time backend bootstrap: detect, and auto-start when local. */
|
|
60
|
+
async function bootstrapBackend() {
|
|
61
|
+
const config = loadConfig()
|
|
62
|
+
const target = config.targetUrl || 'http://127.0.0.1:3080'
|
|
63
|
+
const isLocalDefault = target === 'http://127.0.0.1:3080'
|
|
64
|
+
if (!isLocalDefault || config.autoStartService === false) return
|
|
65
|
+
|
|
66
|
+
const url = await detect()
|
|
67
|
+
if (!url) {
|
|
68
|
+
// Not running — try to start it, but never block window opening.
|
|
69
|
+
try {
|
|
70
|
+
await start({ backendPath: config.backendPath })
|
|
71
|
+
} catch {
|
|
72
|
+
// Backend unavailable; window still opens, tray shows the error.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Reflect the post-detect state in the tray menu.
|
|
76
|
+
refreshTrayMenu()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
app.whenReady().then(async () => {
|
|
80
|
+
await createWindow()
|
|
81
|
+
tray = createTray({
|
|
82
|
+
onShow: () => {
|
|
83
|
+
if (!mainWindow) return createWindow()
|
|
84
|
+
mainWindow.show()
|
|
85
|
+
mainWindow.focus()
|
|
86
|
+
},
|
|
87
|
+
onToggleAutoStart: (enabled) => {
|
|
88
|
+
const config = loadConfig()
|
|
89
|
+
saveConfig({ ...config, autoLaunch: enabled })
|
|
90
|
+
app.setLoginItemSettings({ openAtLogin: enabled })
|
|
91
|
+
},
|
|
92
|
+
onQuit: () => {
|
|
93
|
+
app.isQuitting = true
|
|
94
|
+
app.quit()
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Backend bootstrap in background (never delays the window).
|
|
99
|
+
bootstrapBackend().catch(() => {})
|
|
100
|
+
|
|
101
|
+
app.on('activate', () => {
|
|
102
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
|
103
|
+
else mainWindow?.show()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
app.on('window-all-closed', () => {
|
|
108
|
+
// Keep the app alive in the tray (like a normal desktop utility).
|
|
109
|
+
if (!isMac && !app.isQuitting) {
|
|
110
|
+
// do not quit — tray keeps running
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
app.on('before-quit', () => {
|
|
115
|
+
app.isQuitting = true
|
|
116
|
+
})
|
|
117
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preload: makes the frameless window draggable.
|
|
3
|
+
*
|
|
4
|
+
* A frameless window has no title bar, so dragging is provided by a
|
|
5
|
+
* `-webkit-app-region: drag` strip along the top of the loaded page.
|
|
6
|
+
* The right side is left for the native window-controls overlay.
|
|
7
|
+
*
|
|
8
|
+
* The strip is a transparent overlay, so it never changes page layout —
|
|
9
|
+
* but it sits above the page top edge, so page top-bar buttons can be
|
|
10
|
+
* reached by the strip being only 1px tall at the very edge... Instead we
|
|
11
|
+
* use a pragmatic height and rely on the page's own top padding for the
|
|
12
|
+
* DSH top bar area. Overlap is acceptable: the strip is click-through for
|
|
13
|
+
* everything except dragging (app-region drag areas swallow mouse events).
|
|
14
|
+
*/
|
|
15
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
16
|
+
const platform = process.platform
|
|
17
|
+
const isWin = platform === 'win32'
|
|
18
|
+
const dragHeight = isWin ? 32 : 28
|
|
19
|
+
// Width reserved for native window controls (Win caption buttons / mac
|
|
20
|
+
// traffic lights live at the top-right / top-left).
|
|
21
|
+
const rightReserve = isWin ? 138 : 80
|
|
22
|
+
const leftReserve = isWin ? 0 : 80
|
|
23
|
+
|
|
24
|
+
const strip = document.createElement('div')
|
|
25
|
+
strip.id = 'dsh-clean-shell-drag'
|
|
26
|
+
strip.style.cssText = `
|
|
27
|
+
position: fixed;
|
|
28
|
+
top: 0;
|
|
29
|
+
left: ${leftReserve}px;
|
|
30
|
+
right: ${rightReserve}px;
|
|
31
|
+
height: ${dragHeight}px;
|
|
32
|
+
-webkit-app-region: drag;
|
|
33
|
+
z-index: 2147483647;
|
|
34
|
+
`
|
|
35
|
+
document.body.appendChild(strip)
|
|
36
|
+
})
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local dsh web backend supervision.
|
|
3
|
+
*
|
|
4
|
+
* The shell owns the lifecycle of the dsh backend from the tray:
|
|
5
|
+
* - detect(): probe configured paths + default port
|
|
6
|
+
* - start() / stop() / restart()
|
|
7
|
+
* - status: 'running' | 'stopped' | 'starting' | 'error'
|
|
8
|
+
*
|
|
9
|
+
* Shell/core decoupling: when a remote target URL is configured, no local
|
|
10
|
+
* backend is ever touched — this module only manages the local dsh CLI.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
|
+
import { access } from 'node:fs'
|
|
14
|
+
import { dirname, join } from 'node:path'
|
|
15
|
+
import { loadConfig } from './config.js'
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PORT = 3080
|
|
18
|
+
|
|
19
|
+
let child = null
|
|
20
|
+
let currentStatus = 'stopped'
|
|
21
|
+
let lastError = null
|
|
22
|
+
let startResolver = null
|
|
23
|
+
|
|
24
|
+
export function getStatus() {
|
|
25
|
+
return {
|
|
26
|
+
status: currentStatus,
|
|
27
|
+
port: DEFAULT_PORT,
|
|
28
|
+
url: `http://127.0.0.1:${DEFAULT_PORT}`,
|
|
29
|
+
error: lastError,
|
|
30
|
+
pid: child?.pid ?? null,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Simple HTTP probe — true when something responds on the port. */
|
|
35
|
+
export async function probe(url, timeoutMs = 1500) {
|
|
36
|
+
try {
|
|
37
|
+
const controller = new AbortController()
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
39
|
+
const res = await fetch(url, { signal: controller.signal, redirect: 'follow' })
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
return res.status < 500
|
|
42
|
+
} catch {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Detect a reachable local dsh web service.
|
|
49
|
+
* Returns the URL when something already listens on the port,
|
|
50
|
+
* or null when the backend is down.
|
|
51
|
+
*/
|
|
52
|
+
export async function detect() {
|
|
53
|
+
const url = `http://127.0.0.1:${DEFAULT_PORT}`
|
|
54
|
+
const up = await probe(url)
|
|
55
|
+
if (up) {
|
|
56
|
+
currentStatus = 'running'
|
|
57
|
+
return url
|
|
58
|
+
}
|
|
59
|
+
currentStatus = 'stopped'
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Start the local dsh backend. Resolves once the service answers
|
|
65
|
+
* (or a spawned CLI prints its ready line). Throws on failure.
|
|
66
|
+
*/
|
|
67
|
+
export async function start({ backendPath } = {}) {
|
|
68
|
+
// Already up?
|
|
69
|
+
const up = await detect()
|
|
70
|
+
if (up) return up
|
|
71
|
+
|
|
72
|
+
// Backend path: explicit config → common locations → PATH.
|
|
73
|
+
const resolved = await resolveDshCommand(backendPath)
|
|
74
|
+
if (!resolved) {
|
|
75
|
+
lastError = '未找到 dsh 后端。请在托盘「设置后端文件夹」中指定 dsh CLI 所在目录。'
|
|
76
|
+
currentStatus = 'error'
|
|
77
|
+
throw new Error(lastError)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
currentStatus = 'starting'
|
|
81
|
+
lastError = null
|
|
82
|
+
child = spawn(resolved.command, [...resolved.args, 'web'], {
|
|
83
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
env: { ...process.env, ...(resolved.env || {}) },
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
child.on('error', (err) => {
|
|
89
|
+
lastError = err.message
|
|
90
|
+
currentStatus = 'error'
|
|
91
|
+
if (startResolver) {
|
|
92
|
+
startResolver.reject(new Error(lastError))
|
|
93
|
+
startResolver = null
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
child.on('exit', (code) => {
|
|
98
|
+
child = null
|
|
99
|
+
if (currentStatus === 'starting' && startResolver) {
|
|
100
|
+
lastError = `dsh 后端异常退出 (code ${code})`
|
|
101
|
+
currentStatus = 'error'
|
|
102
|
+
startResolver.reject(new Error(lastError))
|
|
103
|
+
startResolver = null
|
|
104
|
+
} else if (currentStatus !== 'stopped') {
|
|
105
|
+
currentStatus = 'stopped'
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Wait for the ready line with a timeout.
|
|
110
|
+
return await new Promise((resolve, reject) => {
|
|
111
|
+
startResolver = { resolve, reject }
|
|
112
|
+
let stdout = ''
|
|
113
|
+
let stderr = ''
|
|
114
|
+
const timer = setTimeout(() => {
|
|
115
|
+
child?.kill()
|
|
116
|
+
startResolver = null
|
|
117
|
+
lastError = 'dsh 后端启动超时(45s)'
|
|
118
|
+
currentStatus = 'error'
|
|
119
|
+
reject(new Error(lastError))
|
|
120
|
+
}, 45000)
|
|
121
|
+
|
|
122
|
+
const onData = () => {
|
|
123
|
+
const text = stdout + stderr
|
|
124
|
+
const m = text.match(/http:\/\/127\.0\.0\.1:(\d+)/)
|
|
125
|
+
if (m && startResolver) {
|
|
126
|
+
clearTimeout(timer)
|
|
127
|
+
currentStatus = 'running'
|
|
128
|
+
startResolver.resolve(`http://127.0.0.1:${m[1]}`)
|
|
129
|
+
startResolver = null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
child.stdout.on('data', (d) => {
|
|
133
|
+
stdout += d.toString()
|
|
134
|
+
onData()
|
|
135
|
+
})
|
|
136
|
+
child.stderr.on('data', (d) => {
|
|
137
|
+
stderr += d.toString()
|
|
138
|
+
onData()
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Stop the local backend (spawned by us). External instances are left alone. */
|
|
144
|
+
export async function stop() {
|
|
145
|
+
if (!child) {
|
|
146
|
+
currentStatus = 'stopped'
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
const proc = child
|
|
150
|
+
child = null
|
|
151
|
+
currentStatus = 'stopped'
|
|
152
|
+
await new Promise((resolve) => {
|
|
153
|
+
proc.once('exit', resolve)
|
|
154
|
+
proc.kill()
|
|
155
|
+
setTimeout(resolve, 3000)
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Restart the local backend. */
|
|
160
|
+
export async function restart(options) {
|
|
161
|
+
await stop()
|
|
162
|
+
return await start(options)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check whether a folder contains a usable dsh CLI.
|
|
167
|
+
* Returns the resolved executable path, or null.
|
|
168
|
+
*/
|
|
169
|
+
export async function findDshInFolder(folder) {
|
|
170
|
+
if (!folder) return null
|
|
171
|
+
const candidates = [
|
|
172
|
+
joinCmd(folder, 'dsh.cmd'),
|
|
173
|
+
joinCmd(folder, 'dsh'),
|
|
174
|
+
joinCmd(folder, 'bin', 'dsh.cmd'),
|
|
175
|
+
joinCmd(folder, 'node_modules', '.bin', 'dsh.cmd'),
|
|
176
|
+
]
|
|
177
|
+
for (const c of candidates) {
|
|
178
|
+
if (await exists(c)) return c
|
|
179
|
+
}
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Auto-detect the dsh install folder (the folder that contains the dsh
|
|
185
|
+
* CLI), following the same resolution order used to start the backend:
|
|
186
|
+
* PATH → common Windows install locations. Returns the folder path or null.
|
|
187
|
+
*/
|
|
188
|
+
export async function detectInstallFolder() {
|
|
189
|
+
// 1) Explicit backend folder from config.
|
|
190
|
+
const cfgPath = getConfiguredBackendPath()
|
|
191
|
+
if (cfgPath) {
|
|
192
|
+
const found = await findDshInFolder(cfgPath)
|
|
193
|
+
if (found) return dirOf(found)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 2) `dsh` on PATH → resolve where it actually lives.
|
|
197
|
+
const onPath = await commandPath('dsh')
|
|
198
|
+
if (onPath) return dirOf(onPath)
|
|
199
|
+
|
|
200
|
+
// 3) Common locations.
|
|
201
|
+
if (process.platform === 'win32') {
|
|
202
|
+
const candidates = [
|
|
203
|
+
'D:\\deepseek-harness\\prod\\node_modules\\.bin\\dsh.cmd',
|
|
204
|
+
`${process.env.USERPROFILE}\\AppData\\Roaming\\npm\\dsh.cmd`,
|
|
205
|
+
]
|
|
206
|
+
for (const c of candidates) {
|
|
207
|
+
if (await exists(c)) return dirOf(c)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Locate a usable dsh CLI. */
|
|
214
|
+
async function resolveDshCommand(backendPath) {
|
|
215
|
+
// 1) Explicit backend folder from config — look for dsh(.cmd/.exe) inside.
|
|
216
|
+
if (backendPath) {
|
|
217
|
+
const found = await findDshInFolder(backendPath)
|
|
218
|
+
if (found) return { command: found, args: [] }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 2) `dsh` on PATH
|
|
222
|
+
if (await commandExists('dsh')) return { command: 'dsh', args: [] }
|
|
223
|
+
|
|
224
|
+
// 3) Common locations (dev convenience).
|
|
225
|
+
if (process.platform === 'win32') {
|
|
226
|
+
const candidates = [
|
|
227
|
+
'D:\\deepseek-harness\\prod\\node_modules\\.bin\\dsh.cmd',
|
|
228
|
+
`${process.env.USERPROFILE}\\AppData\\Roaming\\npm\\dsh.cmd`,
|
|
229
|
+
]
|
|
230
|
+
for (const c of candidates) {
|
|
231
|
+
if (await exists(c)) return { command: c, args: [] }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function joinCmd(...parts) {
|
|
238
|
+
return parts.join(process.platform === 'win32' ? '\\' : '/')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function dirOf(p) {
|
|
242
|
+
return dirname(p)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getConfiguredBackendPath() {
|
|
246
|
+
try {
|
|
247
|
+
return loadConfig().backendPath || null
|
|
248
|
+
} catch {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Resolve a command on PATH to its absolute path (Windows: where.exe). */
|
|
254
|
+
function commandPath(cmd) {
|
|
255
|
+
return new Promise((resolve) => {
|
|
256
|
+
const finder = process.platform === 'win32' ? 'where' : 'which'
|
|
257
|
+
const c = spawn(finder, [cmd], { windowsHide: true })
|
|
258
|
+
let out = ''
|
|
259
|
+
c.stdout.on('data', (d) => {
|
|
260
|
+
out += d.toString()
|
|
261
|
+
})
|
|
262
|
+
c.on('error', () => resolve(null))
|
|
263
|
+
c.on('exit', () => {
|
|
264
|
+
const line = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean)
|
|
265
|
+
resolve(line || null)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function commandExists(cmd) {
|
|
271
|
+
return new Promise((resolve) => {
|
|
272
|
+
const finder = process.platform === 'win32' ? 'where' : 'which'
|
|
273
|
+
const c = spawn(finder, [cmd], { stdio: 'ignore', windowsHide: true })
|
|
274
|
+
c.on('error', () => resolve(false))
|
|
275
|
+
c.on('exit', (code) => resolve(code === 0))
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function exists(p) {
|
|
280
|
+
return new Promise((resolve) => access(p, (err) => resolve(!err)))
|
|
281
|
+
}
|
package/electron/tray.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System tray: window show/hide, backend management, auto-launch, quit.
|
|
3
|
+
*
|
|
4
|
+
* All backend controls live here (tray only) — the main window stays a
|
|
5
|
+
* pure shell with no backend chrome.
|
|
6
|
+
*/
|
|
7
|
+
import { Tray, Menu, dialog, nativeImage } from 'electron'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { loadConfig, saveConfig } from './config.js'
|
|
11
|
+
import { getStatus, start, stop, restart, detect, findDshInFolder, detectInstallFolder } from './service.js'
|
|
12
|
+
import { checkForUpdate, openRepo, openUrl } from './update.js'
|
|
13
|
+
|
|
14
|
+
const trayIconPath = join(
|
|
15
|
+
fileURLToPath(new URL('.', import.meta.url)),
|
|
16
|
+
'assets',
|
|
17
|
+
process.platform === 'win32' ? 'tray-16.png' : 'tray-32.png',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
let trayInstance = null
|
|
21
|
+
let handlers = null
|
|
22
|
+
|
|
23
|
+
export function createTray({ onShow, onToggleAutoStart, onQuit }) {
|
|
24
|
+
const icon = nativeImage.createFromPath(trayIconPath)
|
|
25
|
+
|
|
26
|
+
handlers = { onShow, onToggleAutoStart, onQuit }
|
|
27
|
+
|
|
28
|
+
trayInstance = new Tray(icon)
|
|
29
|
+
trayInstance.setToolTip('DSH Clean Desktop Shell')
|
|
30
|
+
trayInstance.on('click', onShow)
|
|
31
|
+
|
|
32
|
+
refreshTrayMenu()
|
|
33
|
+
return trayInstance
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Rebuild the context menu (call after backend state changes). */
|
|
37
|
+
export function refreshTrayMenu() {
|
|
38
|
+
if (!trayInstance || !handlers) return
|
|
39
|
+
const { onShow, onToggleAutoStart, onQuit } = handlers
|
|
40
|
+
const config = loadConfig()
|
|
41
|
+
const st = getStatus()
|
|
42
|
+
const label = statusLabel(st)
|
|
43
|
+
|
|
44
|
+
const menu = Menu.buildFromTemplate([
|
|
45
|
+
{ label: '显示 / 打开窗口', click: onShow },
|
|
46
|
+
{ type: 'separator' },
|
|
47
|
+
{ label: `后端:${label}`, enabled: false },
|
|
48
|
+
{
|
|
49
|
+
label: '启动后端',
|
|
50
|
+
enabled: st.status !== 'running' && st.status !== 'starting',
|
|
51
|
+
click: async () => {
|
|
52
|
+
try {
|
|
53
|
+
await start({ backendPath: loadConfig().backendPath })
|
|
54
|
+
} catch (err) {
|
|
55
|
+
dialog.showErrorBox('后端启动失败', err.message)
|
|
56
|
+
}
|
|
57
|
+
refreshTrayMenu()
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: '重启后端',
|
|
62
|
+
enabled: st.status === 'running' || st.status === 'starting',
|
|
63
|
+
click: async () => {
|
|
64
|
+
try {
|
|
65
|
+
await restart({ backendPath: loadConfig().backendPath })
|
|
66
|
+
} catch (err) {
|
|
67
|
+
dialog.showErrorBox('后端重启失败', err.message)
|
|
68
|
+
}
|
|
69
|
+
refreshTrayMenu()
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: '关闭后端',
|
|
74
|
+
enabled: st.status === 'running' || st.status === 'starting',
|
|
75
|
+
click: async () => {
|
|
76
|
+
await stop()
|
|
77
|
+
refreshTrayMenu()
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{ type: 'separator' },
|
|
81
|
+
{
|
|
82
|
+
label: '自动探测后端',
|
|
83
|
+
click: async () => {
|
|
84
|
+
await detect()
|
|
85
|
+
refreshTrayMenu()
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
label: '设置后端安装文件夹…',
|
|
90
|
+
click: async () => {
|
|
91
|
+
// Default location: auto-detect the dsh install folder first.
|
|
92
|
+
const detected = await detectInstallFolder()
|
|
93
|
+
const defaultFolder = detected || config.backendPath || undefined
|
|
94
|
+
const result = await dialog.showOpenDialog({
|
|
95
|
+
title: '选择dsh后端安装文件夹,默认安装用自动探测',
|
|
96
|
+
buttonLabel: '选择此文件夹',
|
|
97
|
+
message: '默认安装用自动探测。可手动指定 dsh 后端安装文件夹(包含 dsh 可执行文件)。',
|
|
98
|
+
properties: ['openDirectory'],
|
|
99
|
+
defaultPath: defaultFolder,
|
|
100
|
+
})
|
|
101
|
+
if (!result.canceled && result.filePaths[0]) {
|
|
102
|
+
const folder = result.filePaths[0]
|
|
103
|
+
saveConfig({ ...loadConfig(), backendPath: folder })
|
|
104
|
+
const found = await findDshInFolder(folder)
|
|
105
|
+
if (found) {
|
|
106
|
+
dialog.showMessageBox({
|
|
107
|
+
type: 'info',
|
|
108
|
+
title: '后端安装文件夹已设置',
|
|
109
|
+
message: `已找到 dsh:\n${found}`,
|
|
110
|
+
})
|
|
111
|
+
} else {
|
|
112
|
+
dialog.showMessageBox({
|
|
113
|
+
type: 'warning',
|
|
114
|
+
title: '未在此文件夹找到 dsh',
|
|
115
|
+
message:
|
|
116
|
+
'此文件夹内未找到 dsh 可执行文件。已保存该路径,但启动后端时可能失败——\n' +
|
|
117
|
+
'请确认选择的是包含 dsh(dsh.cmd / bin/dsh.cmd / node_modules/.bin/dsh.cmd)的文件夹。',
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
refreshTrayMenu()
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{ type: 'separator' },
|
|
125
|
+
{
|
|
126
|
+
label: '检查更新…',
|
|
127
|
+
click: async () => {
|
|
128
|
+
const r = await checkForUpdate()
|
|
129
|
+
if (r.hasUpdate) {
|
|
130
|
+
const choice = dialog.showMessageBoxSync({
|
|
131
|
+
type: 'info',
|
|
132
|
+
title: '发现新版本',
|
|
133
|
+
message: `当前版本 ${r.current},最新版本 ${r.latest}。`,
|
|
134
|
+
detail: '是否前往 GitHub Releases 页面下载?',
|
|
135
|
+
buttons: ['前往下载', '取消'],
|
|
136
|
+
defaultId: 0,
|
|
137
|
+
cancelId: 1,
|
|
138
|
+
})
|
|
139
|
+
if (choice === 0) openUrl(r.url)
|
|
140
|
+
} else if (r.latest) {
|
|
141
|
+
dialog.showMessageBoxSync({
|
|
142
|
+
type: 'info',
|
|
143
|
+
title: '已是最新版本',
|
|
144
|
+
message: `当前版本 ${r.current} 已是最新(${r.latest})。`,
|
|
145
|
+
})
|
|
146
|
+
} else {
|
|
147
|
+
dialog.showMessageBoxSync({
|
|
148
|
+
type: 'warning',
|
|
149
|
+
title: '检查更新失败',
|
|
150
|
+
message: '无法连接 GitHub 检查更新,请检查网络后重试。',
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
label: '仓库主页',
|
|
157
|
+
click: () => openRepo(),
|
|
158
|
+
},
|
|
159
|
+
{ type: 'separator' },
|
|
160
|
+
{
|
|
161
|
+
label: '开机自启',
|
|
162
|
+
type: 'checkbox',
|
|
163
|
+
checked: !!loadConfig().autoLaunch,
|
|
164
|
+
click: (item) => onToggleAutoStart(item.checked),
|
|
165
|
+
},
|
|
166
|
+
{ type: 'separator' },
|
|
167
|
+
{ label: '退出', click: onQuit },
|
|
168
|
+
])
|
|
169
|
+
trayInstance.setContextMenu(menu)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function statusLabel(st) {
|
|
173
|
+
switch (st.status) {
|
|
174
|
+
case 'running':
|
|
175
|
+
return `运行中 (PID ${st.pid ?? '外部'})`
|
|
176
|
+
case 'starting':
|
|
177
|
+
return '启动中…'
|
|
178
|
+
case 'error':
|
|
179
|
+
return `错误:${st.error || '未知'}`
|
|
180
|
+
default:
|
|
181
|
+
return '未运行'
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version check and repo links.
|
|
3
|
+
*
|
|
4
|
+
* Checks the latest GitHub release of dsh-clean-desktop-shell against the
|
|
5
|
+
* local app version (from app.getVersion()). Network failures are treated
|
|
6
|
+
* as "no update" — never block or error in the tray.
|
|
7
|
+
*/
|
|
8
|
+
import { app, shell } from 'electron'
|
|
9
|
+
|
|
10
|
+
const REPO_URL = 'https://github.com/Icather/dsh-clean-desktop-shell'
|
|
11
|
+
const RELEASES_API = 'https://api.github.com/repos/Icather/dsh-clean-desktop-shell/releases/latest'
|
|
12
|
+
|
|
13
|
+
/** Parse "v1.2.3" / "1.2.3" → [1,2,3]; null when malformed. */
|
|
14
|
+
function parseVersion(v) {
|
|
15
|
+
if (!v) return null
|
|
16
|
+
const m = String(v).replace(/^v/i, '').trim().match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
17
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when a is strictly newer than b. */
|
|
21
|
+
function isNewer(a, b) {
|
|
22
|
+
if (!a || !b) return false
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
if (a[i] !== b[i]) return a[i] > b[i]
|
|
25
|
+
}
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check for a newer release.
|
|
31
|
+
* @returns {{ hasUpdate: boolean, latest?: string, current: string, url: string }}
|
|
32
|
+
*/
|
|
33
|
+
export async function checkForUpdate() {
|
|
34
|
+
const current = app.getVersion()
|
|
35
|
+
let latest = null
|
|
36
|
+
let tag = null
|
|
37
|
+
let url = REPO_URL
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const controller = new AbortController()
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), 8000)
|
|
42
|
+
const res = await fetch(RELEASES_API, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
45
|
+
})
|
|
46
|
+
clearTimeout(timer)
|
|
47
|
+
if (res.ok) {
|
|
48
|
+
const data = await res.json()
|
|
49
|
+
tag = data.tag_name || null
|
|
50
|
+
latest = parseVersion(tag)
|
|
51
|
+
if (data.html_url) url = data.html_url
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Network error — no update known.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const currentV = parseVersion(current)
|
|
58
|
+
return {
|
|
59
|
+
hasUpdate: isNewer(latest, currentV),
|
|
60
|
+
latest: tag,
|
|
61
|
+
current,
|
|
62
|
+
url,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Open the repository homepage in the default browser. */
|
|
67
|
+
export function openRepo() {
|
|
68
|
+
shell.openExternal(REPO_URL)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Open an arbitrary URL in the default browser. */
|
|
72
|
+
export function openUrl(url) {
|
|
73
|
+
shell.openExternal(url)
|
|
74
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clean window creation — no frosted-glass materials.
|
|
3
|
+
*
|
|
4
|
+
* Pure shell philosophy: the window is a normal native frame with
|
|
5
|
+
* window-controls overlay (Win) / hiddenInset (mac), and nothing else.
|
|
6
|
+
* No Mica, no vibrancy — keep it clean.
|
|
7
|
+
*/
|
|
8
|
+
import { BrowserWindow } from 'electron'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
export const WINDOWS_TITLEBAR_HEIGHT = 32
|
|
13
|
+
|
|
14
|
+
const PRELOAD_PATH = fileURLToPath(new URL('./preload.js', import.meta.url))
|
|
15
|
+
// Black-whale app icon (matches the DSH web favicon).
|
|
16
|
+
const ICON_PATH = fileURLToPath(new URL('../build/icon.png', import.meta.url))
|
|
17
|
+
|
|
18
|
+
export function createMainWindow({ target }) {
|
|
19
|
+
const platform = process.platform
|
|
20
|
+
const isWin = platform === 'win32'
|
|
21
|
+
const isMac = platform === 'darwin'
|
|
22
|
+
|
|
23
|
+
const base = {
|
|
24
|
+
width: 1280,
|
|
25
|
+
height: 800,
|
|
26
|
+
minWidth: 760,
|
|
27
|
+
minHeight: 520,
|
|
28
|
+
show: false,
|
|
29
|
+
title: 'DeepSeek Harness',
|
|
30
|
+
backgroundColor: '#10131A',
|
|
31
|
+
icon: isWin ? ICON_PATH : undefined,
|
|
32
|
+
webPreferences: {
|
|
33
|
+
preload: PRELOAD_PATH,
|
|
34
|
+
contextIsolation: true,
|
|
35
|
+
nodeIntegration: false,
|
|
36
|
+
sandbox: true,
|
|
37
|
+
webSecurity: true,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let options = { ...base }
|
|
42
|
+
|
|
43
|
+
if (isMac) {
|
|
44
|
+
options = {
|
|
45
|
+
...base,
|
|
46
|
+
titleBarStyle: 'hiddenInset',
|
|
47
|
+
trafficLightPosition: { x: 16, y: 16 },
|
|
48
|
+
}
|
|
49
|
+
} else if (isWin) {
|
|
50
|
+
options = {
|
|
51
|
+
...base,
|
|
52
|
+
autoHideMenuBar: true,
|
|
53
|
+
titleBarStyle: 'hidden',
|
|
54
|
+
titleBarOverlay: {
|
|
55
|
+
color: '#00000000',
|
|
56
|
+
symbolColor: '#7f858f',
|
|
57
|
+
height: WINDOWS_TITLEBAR_HEIGHT,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Linux / other: keep the native frame.
|
|
62
|
+
|
|
63
|
+
const win = new BrowserWindow(options)
|
|
64
|
+
win.loadURL(target)
|
|
65
|
+
|
|
66
|
+
win.once('ready-to-show', () => win.show())
|
|
67
|
+
|
|
68
|
+
return win
|
|
69
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
|
+
*
|
|
4
|
+
* Registers a settings row so the shell's target URL / material options
|
|
5
|
+
* are editable from Settings → General. This is a minimal placeholder:
|
|
6
|
+
* real UI wiring lands with the Electron shell work.
|
|
7
|
+
*/
|
|
8
|
+
export function apply() {
|
|
9
|
+
// placeholder: settings-row wiring arrives with the shell implementation
|
|
10
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — host half (plugin loader entry).
|
|
3
|
+
*
|
|
4
|
+
* The desktop shell is an Electron client; the host half only registers
|
|
5
|
+
* the plugin so it mounts cleanly into a dsh profile and exposes its
|
|
6
|
+
* settings surface. Window material (Mica / vibrancy) lives in the
|
|
7
|
+
* Electron main process (see src/main/).
|
|
8
|
+
*/
|
|
9
|
+
export function apply(ctx) {
|
|
10
|
+
ctx.on('ready', () => {
|
|
11
|
+
ctx.logger.info('[clean-desktop-shell] mounted (host half)')
|
|
12
|
+
})
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-clean-desktop-shell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — reuses your web profile, tray/single-instance/auto-launch, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/",
|
|
9
|
+
"electron/",
|
|
10
|
+
"src/",
|
|
11
|
+
"scripts/",
|
|
12
|
+
"build/icon.png",
|
|
13
|
+
"cordis.patch.yml",
|
|
14
|
+
"README.md",
|
|
15
|
+
"README.en.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"version.txt"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./lib/index.js",
|
|
21
|
+
"./client": "./lib/client.js",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "node scripts/build.mjs",
|
|
26
|
+
"check": "node --check lib/index.js",
|
|
27
|
+
"dev": "electron electron/main.js",
|
|
28
|
+
"icons": "node scripts/gen-icons.mjs",
|
|
29
|
+
"pack": "electron-builder --win nsis",
|
|
30
|
+
"pack:mac": "electron-builder --mac dmg"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"inject": [
|
|
38
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
39
|
+
"@deepseek-ai/dsh-client-locale",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-theme"
|
|
42
|
+
],
|
|
43
|
+
"platform": "web"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/dsh": "*"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@deepseek-ai/dsh": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"electron": "^33.0.0",
|
|
56
|
+
"electron-builder": "^25.0.0",
|
|
57
|
+
"png-to-ico": "^3.0.2",
|
|
58
|
+
"sharp": "^0.35.3"
|
|
59
|
+
},
|
|
60
|
+
"build": {
|
|
61
|
+
"appId": "com.icather.dsh-clean-desktop-shell",
|
|
62
|
+
"productName": "DSH Clean Desktop Shell",
|
|
63
|
+
"asar": true,
|
|
64
|
+
"files": [
|
|
65
|
+
"electron/**/*",
|
|
66
|
+
"lib/**/*",
|
|
67
|
+
"src/**/*",
|
|
68
|
+
"build/icon.png",
|
|
69
|
+
"package.json",
|
|
70
|
+
"cordis.patch.yml"
|
|
71
|
+
],
|
|
72
|
+
"extraMetadata": {
|
|
73
|
+
"main": "electron/main.js"
|
|
74
|
+
},
|
|
75
|
+
"win": {
|
|
76
|
+
"target": [
|
|
77
|
+
{
|
|
78
|
+
"target": "nsis",
|
|
79
|
+
"arch": [
|
|
80
|
+
"x64"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"icon": "build/icon.ico",
|
|
85
|
+
"signAndEditExecutable": false
|
|
86
|
+
},
|
|
87
|
+
"mac": {
|
|
88
|
+
"target": [
|
|
89
|
+
"dmg"
|
|
90
|
+
],
|
|
91
|
+
"category": "public.app-category.developer-tools"
|
|
92
|
+
},
|
|
93
|
+
"nsis": {
|
|
94
|
+
"oneClick": false,
|
|
95
|
+
"allowToChangeInstallationDirectory": true,
|
|
96
|
+
"createDesktopShortcut": true,
|
|
97
|
+
"createStartMenuShortcut": true
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"license": "MIT",
|
|
101
|
+
"allowScripts": {
|
|
102
|
+
"electron@33.4.11": true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Minimal zero-dependency build: copies src → lib.
|
|
3
|
+
// Real bundling (tsdown/vite) lands with the Electron shell work.
|
|
4
|
+
import { cpSync, mkdirSync } from 'node:fs'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
9
|
+
mkdirSync(join(root, 'lib'), { recursive: true })
|
|
10
|
+
cpSync(join(root, 'src', 'host', 'index.js'), join(root, 'lib', 'index.js'))
|
|
11
|
+
cpSync(join(root, 'src', 'client', 'client.js'), join(root, 'lib', 'client.js'))
|
|
12
|
+
console.log('[dsh-clean-desktop-shell] built lib/ from src/')
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate app icons from the DSH favicon whale SVG.
|
|
4
|
+
* - build/icon.png 256x256 (source for ICO and README)
|
|
5
|
+
* - build/icon.ico multi-size (16/24/32/48/64/128/256) for electron-builder win
|
|
6
|
+
* - build/icon.icns (mac; generated by electron-builder if missing — we emit a
|
|
7
|
+
* placeholder note; real icns needs mac tooling)
|
|
8
|
+
* - electron/assets/tray.png 16x16 + 32x32 white-on-transparent for the tray
|
|
9
|
+
*/
|
|
10
|
+
import sharp from 'sharp'
|
|
11
|
+
import pngToIco from 'png-to-ico'
|
|
12
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
17
|
+
const buildDir = join(root, 'build')
|
|
18
|
+
const assetsDir = join(root, 'electron', 'assets')
|
|
19
|
+
mkdirSync(buildDir, { recursive: true })
|
|
20
|
+
mkdirSync(assetsDir, { recursive: true })
|
|
21
|
+
|
|
22
|
+
const SVG = join(buildDir, 'favicon-source.svg')
|
|
23
|
+
|
|
24
|
+
// Dark variant: white whale on transparent (for dark tray / dark backgrounds).
|
|
25
|
+
const svgLight = `<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 50 50">
|
|
26
|
+
<path d="M48.8354 10.0479C48.3232 9.79199 48.1025 10.2798 47.8032 10.5278C47.7007 10.6079 47.6143 10.7119 47.5273 10.8076C46.7793 11.624 45.9048 12.1597 44.7622 12.0957C43.0923 12 41.666 12.5356 40.4058 13.8398C40.1377 12.2319 39.2476 11.272 37.8926 10.6558C37.1836 10.3359 36.4668 10.0156 35.9702 9.31982C35.6235 8.82373 35.5293 8.27197 35.356 7.72754C35.2456 7.3999 35.1353 7.06396 34.7651 7.00781C34.3633 6.94385 34.2056 7.2876 34.0479 7.57568C33.418 8.75195 33.1733 10.0479 33.1973 11.3599C33.2524 14.312 34.4736 16.6641 36.8999 18.3359C37.1758 18.5278 37.2466 18.7197 37.1597 19C36.9946 19.5757 36.7974 20.1357 36.624 20.7119C36.5137 21.0801 36.3486 21.1597 35.9624 21C34.6309 20.4321 33.481 19.5918 32.4644 18.5757C30.7393 16.8721 29.1792 14.9917 27.2334 13.52C26.7764 13.1758 26.3193 12.856 25.8467 12.5518C23.8618 10.584 26.1069 8.96777 26.627 8.77588C27.1704 8.57568 26.8159 7.8877 25.0591 7.896C23.3022 7.90381 21.6953 8.50391 19.647 9.30371C19.3477 9.42383 19.0322 9.51172 18.7095 9.58398C16.8501 9.22363 14.9199 9.14355 12.9033 9.37598C9.10596 9.80762 6.07275 11.6396 3.84326 14.7681C1.16455 18.5278 0.53418 22.7998 1.30664 27.2559C2.11768 31.9521 4.46582 35.8398 8.07373 38.8799C11.8159 42.0322 16.1255 43.5762 21.041 43.2803C24.0269 43.104 27.3516 42.6963 31.1016 39.4561C32.0469 39.936 33.0396 40.1279 34.686 40.272C35.9546 40.3921 37.1758 40.208 38.1211 40.0078C39.6021 39.688 39.4995 38.2881 38.9639 38.0322C34.623 35.9678 35.5762 36.8081 34.71 36.1279C36.9155 33.4639 40.2402 30.6958 41.54 21.728C41.6426 21.0161 41.5557 20.5679 41.54 19.9917C41.5322 19.6396 41.6108 19.5039 42.0049 19.4639C43.0923 19.3359 44.1479 19.0317 45.1167 18.4878C47.9292 16.9199 49.064 14.3438 49.3315 11.2559C49.3711 10.7837 49.3237 10.2959 48.8354 10.0479ZM24.3262 37.8398C20.1196 34.4639 18.0791 33.3521 17.2358 33.3999C16.4482 33.4482 16.5898 34.3682 16.7632 34.9678C16.9443 35.5601 17.1812 35.9683 17.5117 36.4878C17.7402 36.832 17.8979 37.3442 17.2832 37.728C15.9282 38.584 13.5728 37.4399 13.4624 37.3838C10.7207 35.7358 8.42822 33.5601 6.81348 30.584C5.25342 27.7197 4.34766 24.6479 4.19775 21.3677C4.1582 20.5757 4.38672 20.2959 5.15869 20.1519C6.17529 19.96 7.22314 19.9199 8.23926 20.0718C12.5327 20.7119 16.1885 22.6719 19.2529 25.7759C21.002 27.5439 22.3252 29.6558 23.6885 31.7202C25.1377 33.9121 26.6978 36 28.6831 37.7119C29.3843 38.312 29.9434 38.7681 30.479 39.104C28.8643 39.2881 26.1699 39.3281 24.3262 37.8398ZM26.3433 24.6001C26.3433 24.248 26.6191 23.9678 26.9658 23.9678C27.0444 23.9678 27.1152 23.9839 27.1782 24.0078C27.2651 24.04 27.3438 24.0879 27.4067 24.1602C27.5171 24.272 27.5801 24.4321 27.5801 24.6001C27.5801 24.9521 27.3042 25.2319 26.9575 25.2319C26.6108 25.2319 26.3433 24.9521 26.3433 24.6001ZM32.6064 27.8799C32.2046 28.0479 31.8027 28.1919 31.4165 28.208C30.8179 28.2397 30.1641 27.9922 29.8096 27.688C29.2583 27.2158 28.8643 26.9521 28.6987 26.1279C28.6279 25.7759 28.6675 25.2319 28.7305 24.9199C28.8721 24.248 28.7144 23.8159 28.2495 23.4238C27.8716 23.104 27.3911 23.0161 26.8633 23.0161C26.666 23.0161 26.4849 22.9277 26.3511 22.856C26.1304 22.7441 25.9492 22.4639 26.1226 22.1201C26.1777 22.0078 26.4458 21.7358 26.5088 21.688C27.2256 21.272 28.0527 21.4077 28.8169 21.7197C29.5259 22.0161 30.0615 22.5601 30.834 23.3281C31.6216 24.2559 31.7632 24.5117 32.2124 25.208C32.5669 25.752 32.8901 26.312 33.1104 26.9521C33.2446 27.3521 33.0713 27.6802 32.6064 27.8799Z" fill="#fff" fill-opacity="1.000000" fill-rule="nonzero"/>
|
|
27
|
+
</svg>`
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
// 512px app icon (black whale — matches DSH dark theme). macOS requires
|
|
31
|
+
// >=512px for the app icon.
|
|
32
|
+
await sharp(SVG, { density: 300 })
|
|
33
|
+
.resize(512, 512, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
34
|
+
.png()
|
|
35
|
+
.toFile(join(buildDir, 'icon.png'))
|
|
36
|
+
|
|
37
|
+
// Multi-size ICO — capped at 256: NSIS's installer icon loader rejects
|
|
38
|
+
// 512px frames ("invalid icon file size"), and Windows doesn't need them.
|
|
39
|
+
const sizes = [16, 24, 32, 48, 64, 128, 256]
|
|
40
|
+
const pngs = []
|
|
41
|
+
for (const s of sizes) {
|
|
42
|
+
const f = join(buildDir, `icon-${s}.png`)
|
|
43
|
+
await sharp(SVG, { density: 300 })
|
|
44
|
+
.resize(s, s, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
45
|
+
.png()
|
|
46
|
+
.toFile(f)
|
|
47
|
+
pngs.push(f)
|
|
48
|
+
}
|
|
49
|
+
const ico = await pngToIco(pngs)
|
|
50
|
+
writeFileSync(join(buildDir, 'icon.ico'), ico)
|
|
51
|
+
for (const f of pngs) rmSync(f)
|
|
52
|
+
|
|
53
|
+
// Tray: white whale 16 and 32 (visible on dark tray).
|
|
54
|
+
for (const s of [16, 32]) {
|
|
55
|
+
await sharp(Buffer.from(svgLight), { density: 300 })
|
|
56
|
+
.resize(s, s, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
57
|
+
.png()
|
|
58
|
+
.toFile(join(assetsDir, `tray-${s}.png`))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log('icons generated:')
|
|
62
|
+
console.log(' build/icon.png (512)')
|
|
63
|
+
console.log(' build/icon.ico (16..256, NSIS-safe)')
|
|
64
|
+
console.log(' electron/assets/tray-16.png, tray-32.png (white whale)')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((e) => {
|
|
68
|
+
console.error(e)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
|
+
*
|
|
4
|
+
* Registers a settings row so the shell's target URL / material options
|
|
5
|
+
* are editable from Settings → General. This is a minimal placeholder:
|
|
6
|
+
* real UI wiring lands with the Electron shell work.
|
|
7
|
+
*/
|
|
8
|
+
export function apply() {
|
|
9
|
+
// placeholder: settings-row wiring arrives with the shell implementation
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-clean-desktop-shell — host half (plugin loader entry).
|
|
3
|
+
*
|
|
4
|
+
* The desktop shell is an Electron client; the host half only registers
|
|
5
|
+
* the plugin so it mounts cleanly into a dsh profile and exposes its
|
|
6
|
+
* settings surface. Window material (Mica / vibrancy) lives in the
|
|
7
|
+
* Electron main process (see src/main/).
|
|
8
|
+
*/
|
|
9
|
+
export function apply(ctx) {
|
|
10
|
+
ctx.on('ready', () => {
|
|
11
|
+
ctx.logger.info('[clean-desktop-shell] mounted (host half)')
|
|
12
|
+
})
|
|
13
|
+
}
|
package/version.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|