dsh-clean-desktop-shell 0.1.3 → 0.1.5

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/README.en.md CHANGED
@@ -29,24 +29,43 @@ Key differences from other desktop clients in the ecosystem:
29
29
  | **Visual changes** | Custom title bar / frosted glass etc. | **None** — pure window shell |
30
30
  | **Upstream** | Pinned version | **Tracks rc.7** |
31
31
 
32
- ## Install
32
+ ## Usage
33
33
 
34
- > **Status**: plugin-market install (Option 2) is under construction, expected live early tomorrow. **Only Option 1 (the installer) is currently available.**
34
+ 1. If the plugin is installed, launching `dsh` from the command line pops up the desktop window automatically; you can also double-click the desktop shortcut created by the plugin — on par with a native desktop app.
35
+ 2. Everything from the original web UI works as-is.
36
+ 3. Detailed settings live in the tray right-click menu. The main window adds no controls of its own, keeping the page clean.
37
+
38
+ **All backend controls live in the tray** — the main window stays a pure shell:
35
39
 
36
- **Option 1: download the installer from Releases (recommended for end users the only full desktop-app install)**
40
+ - Start / restart / stop the backend (with progress dialogs; stopping really shuts down the service on 3080, including externally started instances)
41
+ - Auto-detect backend · set the backend install folder (auto-detect default)
42
+ - Reload window · create desktop shortcut · check for updates · repo homepage
43
+
44
+ **Window reliability (Edge-style instant refresh):**
45
+
46
+ - Shows immediately on launch, never waits for the backend
47
+ - While the backend is down, a local "backend offline" screen is shown and re-probed; the real page loads automatically the moment it answers
48
+ - The instant the backend stops (tray stop, kill or crash) the window flips back to the offline screen — a stale page never fakes "still alive"
49
+ - The offline screen has self-service buttons: reload / start backend / auto-detect backend / set backend install folder
50
+
51
+ ## Install
52
+
53
+ **Option 1: download the installer from Releases (for a standalone desktop app)**
37
54
 
38
55
  - Windows: `DSH-Clean-Desktop-Shell-Setup-<version>.exe`
39
56
  - macOS: `DSH-Clean-Desktop-Shell-<version>.dmg` (Intel) or `-arm64.dmg` (Apple Silicon)
40
57
 
41
58
  The installer **creates a desktop shortcut automatically** and provides the full desktop experience (tray). The first time you run the Windows installer you may see a SmartScreen warning — **this is normal for unsigned programs, not a virus**, see "Windows SmartScreen warning" below.
42
59
 
43
- **Option 2: register as a DSH plugin (developers/power users only) — ⚠️ this command does NOT install any desktop app**
60
+ **Option 2: install as a DSH plugin (DSH ecosystem users)**
44
61
 
45
62
  ```sh
46
63
  dsh plugin --profile web add dsh-clean-desktop-shell
47
64
  ```
48
65
 
49
- > **Important**: Option 2 only "registers" the shell into your DSH profile. **No desktop app appears on your machine** no installer, no desktop icon, no tray. It is meant for developers who want the shell mounted in the DSH ecosystem (reuses your web profile config, future settings integration). **For a double-clickable desktop app, use Option 1** (or run the source with `npm run dev`).
66
+ Restart `dsh web` and the desktop shell window **opens automatically** (the first run prepares the Electron runtime over the network, ~1-2 minutes).
67
+
68
+ > Option 2 gives you a shell that launches alongside DSH: the window is spawned by the plugin when `dsh web` starts, with **no standalone installer / desktop icon**. For a double-clickable app with a desktop shortcut and auto-update, use Option 1. The core window experience is identical either way.
50
69
 
51
70
  > The shell needs a reachable `dsh web` service (local or configured remote address). See Usage.
52
71
 
@@ -87,22 +106,30 @@ Unblock-File -Path "$env:USERPROFILE\Downloads\DSH-Clean-Desktop-Shell-Setup-*.e
87
106
  ## Architecture
88
107
 
89
108
  ```
90
- ┌─────────────────── Core (dsh web / headless service) ───────────────────┐
91
- Sessions · Agent · Plugins · Memory live here, decoupled from UI
92
- └─────────────────────────────────────────────────────────────────────────┘
93
-
94
- ┌───────────────┴───────────────┐
95
- │ dsh-clean-desktop-shell │
96
- │ Electron shell (client) │
97
- tray · single-instance ·
98
-
99
- └────────────────────────────────┘
109
+ ┌────────────── Core (dsh web / headless service) ──────────────┐
110
+ Sessions · Agent · Plugins · Memory live here,
111
+ │ decoupled from the UI │
112
+ └───────────────────────────┬───────────────────────────────────┘
113
+ │ http://127.0.0.1:3080 (or remote)
114
+
115
+ ┌───────────────────────────────────────────────────────────┐
116
+ dsh-clean-desktop-shell (Electron shell)
117
+ tray · single-instance · offline auto-reconnect
118
+ │ · desktop shortcut │
119
+ │ │
120
+ │ One shell codebase, two distribution forms: │
121
+ │ ├─ Installer: standalone exe, auto-update │
122
+ │ └─ Plugin: auto-pops on dsh web start │
123
+ │ (self-managed Electron runtime) │
124
+ └───────────────────────────────────────────────────────────┘
100
125
  ```
101
126
 
127
+ - **Shell / core separation**: the shell handles only the window, tray and backend management; sessions, agents, plugins and memory all live in the core, decoupled from the UI.
102
128
  - **Default**: loads the local `127.0.0.1:3080` (your configured web profile, zero migration).
103
129
  - **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.
130
+ - **One shell codebase, two distribution forms**: the installer (standalone exe) and the plugin (launches with `dsh web`) share the same `electron/` code — only the runtime source and launch differ (see Install).
104
131
 
105
- ### Platform matrix
132
+ ## Platform matrix
106
133
 
107
134
  | Platform | Shell | Status |
108
135
  |:--|:--|:--|
@@ -111,29 +138,6 @@ Unblock-File -Path "$env:USERPROFILE\Downloads\DSH-Clean-Desktop-Shell-Setup-*.e
111
138
  | Linux | — (browser / PWA to the core) | Not planned |
112
139
  | Termux / phone / tablet | — (headless / PWA to the core) | Covered by remote core access |
113
140
 
114
- ## Usage
115
-
116
- 1. Start `dsh web` (or configure a remote service address).
117
- 2. Launch the shell: it auto-detects local 3080 — loads the page if the backend is up, otherwise shows the "backend offline" screen where you can start it in one click.
118
- 3. Drag the window by its top area (right side reserved for native buttons); close minimizes to tray by default.
119
-
120
- **All backend controls live in the tray** — the main window stays a pure shell:
121
-
122
- - Start / restart / stop the backend (with progress dialogs; stopping really
123
- shuts down the service on 3080, including externally started instances)
124
- - Auto-detect backend · set the backend install folder (auto-detect default)
125
- - Reload window · check for updates · repo homepage
126
-
127
- **Window reliability (Edge-style instant refresh):**
128
-
129
- - Shows immediately on launch, never waits for the backend
130
- - While the backend is down, a local "backend offline" screen is shown and
131
- re-probed; the real page loads automatically the moment it answers
132
- - The instant the backend stops (tray stop, kill or crash) the window flips
133
- back to the offline screen — a stale page never fakes "still alive"
134
- - The offline screen has self-service buttons: reload / start backend /
135
- auto-detect backend / set backend install folder
136
-
137
141
  ## Development
138
142
 
139
143
  ```sh
@@ -145,6 +149,17 @@ npm run pack # package NSIS (Win) / DMG (mac)
145
149
 
146
150
  ## Changelog
147
151
 
152
+ ### 0.1.4
153
+ - Branch 2 (plugin-market distribution) is now live: `dsh plugin add` →
154
+ restart `dsh web` → the desktop shell opens automatically. The Electron
155
+ runtime is self-provisioned by the plugin (local reuse / network-aware
156
+ source selection).
157
+ - Desktop shortcut: first-run prompt + one-click "create desktop shortcut"
158
+ in the tray (both the installer and plugin forms).
159
+ - Icons: Windows taskbar and macOS Dock show the whale icon in bare-runtime
160
+ (plugin) mode.
161
+ - Auto-launch (login item) removed — both forms are now fully manual.
162
+
148
163
  ### 0.1.2
149
164
  - Windows auto-update: tray "check for updates" now downloads in the
150
165
  background with progress and installs on restart (electron-updater);
package/README.md CHANGED
@@ -29,24 +29,43 @@
29
29
  | **视觉改造** | 自绘标题栏 / 毛玻璃等 | **零改造**,纯净窗口壳 |
30
30
  | **跟随上游** | 固定版本 | **跟随 rc.7** |
31
31
 
32
- ## 安装
32
+ ## 使用
33
33
 
34
- > **状态提示**:插件形式安装(方式二)施工中,预计今日凌晨上线。**当前仅支持方式一安装包安装**。
34
+ 1. 若安装过插件,命令行启动 `dsh` 自动弹出桌面窗口;也可通过插件创建的桌面快捷方式双击,媲美原生桌面端的体验。
35
+ 2. 使用原网页端的一切功能。
36
+ 3. 托盘右键可以进行详细设置。主窗口不添加任何控件,保持页面纯净。
35
37
 
36
- **方式一:从 Release 下载安装包(推荐普通用户,唯一完整的桌面应用安装方式)**
38
+ **后端的一切操作都在托盘右键**,主窗口保持纯壳:
39
+
40
+ - 启动 / 重启 / 关闭后端(带进度弹窗;关闭会真正停掉 3080 上的服务,包括外部启动的实例)
41
+ - 自动探测后端 · 设置后端安装文件夹(默认自动探测定位)
42
+ - 刷新窗口 · 创建桌面快捷方式 · 检查更新 · 仓库主页
43
+
44
+ **窗口的可靠性(Edge 式即时刷新)**:
45
+
46
+ - 双击启动立即出窗,不等后端就绪
47
+ - 后端没起来时显示「后端未连接」页,自动探测;后端一通立即加载
48
+ - 后端关闭 / 被杀的一刻,窗口立刻切回离线页——不会停在旧页面假装还活着
49
+ - 离线页内置快捷按钮:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹
50
+
51
+ ## 安装
52
+
53
+ **方式一:从 Release 下载安装包(想要独立桌面应用的用户)**
37
54
 
38
55
  - Windows:下载 `DSH-Clean-Desktop-Shell-Setup-<版本>.exe`
39
56
  - macOS:下载 `DSH-Clean-Desktop-Shell-<版本>.dmg`(Intel)或 `-arm64.dmg`(Apple Silicon)
40
57
 
41
58
  安装包会**自动创建桌面快捷方式**,并提供系统托盘等完整桌面体验。首次运行 Windows 安装包可能触发 SmartScreen 警告——**这是未签名程序的正常现象,不是病毒**,见下方「Windows SmartScreen 警告说明」。
42
59
 
43
- **方式二:作为 DSH 插件注册(仅开发者/高级用户)——⚠️ 这个命令不会安装任何桌面应用**
60
+ **方式二:作为 DSH 插件安装(DSH 生态用户)**
44
61
 
45
62
  ```sh
46
63
  dsh plugin --profile web add dsh-clean-desktop-shell
47
64
  ```
48
65
 
49
- > **重要**:方式二只是把壳"登记"进 DSH profile,**你的电脑上不会多出任何桌面应用**——没有安装包、没有桌面图标、没有系统托盘。它只用于开发者把壳挂进 DSH 生态(复用 web profile 配置、供未来设置集成)。**想要可双击使用的桌面应用,请用方式一**(或源码 `npm run dev` 运行)。
66
+ 重启 `dsh web` 后,桌面壳窗口会**自动弹出**(首次运行需联网准备 Electron 运行时,约 1-2 分钟)。
67
+
68
+ > 方式二得到的是"随 DSH 启动的桌面壳":窗口由插件在 `dsh web` 启动时自动拉起,**不产生独立安装包 / 桌面图标**。想要可双击启动、带桌面快捷方式和自动更新的独立应用,请用方式一。两种方式的核心窗口体验一致。
50
69
 
51
70
  > 桌面壳需要本机有可用的 `dsh web` 服务(或配置的远程地址)。见下方「使用」。
52
71
 
@@ -91,21 +110,27 @@ Unblock-File -Path "$env:USERPROFILE\Downloads\DSH-Clean-Desktop-Shell-Setup-*.e
91
110
  ## 架构
92
111
 
93
112
  ```
94
- ┌──────────────────── 内核(dsh web / headless 服务) ────────────────────┐
95
- 会话 · Agent · 插件 · 记忆 都在这层,与界面解耦
96
- └─────────────────────────────────────────────────────────────────────────┘
97
-
98
- ┌───────────────┴───────────────┐
99
- │ dsh-clean-desktop-shell │
100
- Electron 壳(客户端)
101
- 托盘 · 单实例
102
- └────────────────────────────────┘
113
+ ┌────────────── 内核(dsh web / headless 服务)──────────────┐
114
+ 会话 · Agent · 插件 · 记忆 都在这层,与界面解耦
115
+ └───────────────────────────┬──────────────────────────────┘
116
+ │ http://127.0.0.1:3080(或远程地址)
117
+
118
+ ┌───────────────────────────────────────────────────────────┐
119
+ dsh-clean-desktop-shell(Electron 壳)
120
+ 托盘 · 单实例 · 离线自动重连 · 桌面快捷方式
121
+ │ │
122
+ │ 同一份壳代码,两种分发形态: │
123
+ │ ├─ 安装包版:独立 exe,双击即用,自动更新 │
124
+ │ └─ 插件版:随 dsh web 启动自动弹窗(自管 Electron 运行时)│
125
+ └───────────────────────────────────────────────────────────┘
103
126
  ```
104
127
 
128
+ - **壳 / 内核分离**:壳只负责窗口、托盘、后端管理;会话、Agent、插件、记忆都在内核层,与界面解耦。
105
129
  - **默认**:加载本地 `127.0.0.1:3080`(已配置好的 web profile,零迁移)。
106
130
  - **可配远程**:在设置里填入任意远程 DSH 地址,壳只当窗口——手机 / Linux / 其他设备通过浏览器或 PWA 也能接入内核,壳本身不绑定本地服务。
131
+ - **同一套壳代码,两种分发形态**:安装包版(独立 exe)与插件版(随 `dsh web` 启动)共用 `electron/` 壳代码,仅运行时来源与启动方式不同(见「安装」)。
107
132
 
108
- ### 平台矩阵
133
+ ## 平台矩阵
109
134
 
110
135
  | 平台 | 壳 | 状态 |
111
136
  |:--|:--|:--|
@@ -114,25 +139,6 @@ Unblock-File -Path "$env:USERPROFILE\Downloads\DSH-Clean-Desktop-Shell-Setup-*.e
114
139
  | Linux | —(浏览器 / PWA 直连内核) | 不做 |
115
140
  | Termux / 手机 / 平板 | —(headless / PWA 直连内核) | 由内核远程访问支持 |
116
141
 
117
- ## 使用
118
-
119
- 1. 启动 `dsh web`(或配置远程服务地址)。
120
- 2. 启动桌面壳:自动检测本地 3080——后端在跑则直接加载,未运行则显示「后端未连接」页,可在离线页一键启动。
121
- 3. 窗口顶部(右侧留系统按钮)可拖动窗口;关闭默认缩到托盘。
122
-
123
- **后端的一切操作都在托盘右键**,主窗口保持纯壳:
124
-
125
- - 启动 / 重启 / 关闭后端(带进度弹窗;关闭会真正停掉 3080 上的服务,包括外部启动的实例)
126
- - 自动探测后端 · 设置后端安装文件夹(默认自动探测定位)
127
- - 刷新窗口 · 检查更新 · 仓库主页
128
-
129
- **窗口的可靠性(Edge 式即时刷新)**:
130
-
131
- - 双击启动立即出窗,不等后端就绪
132
- - 后端没起来时显示「后端未连接」页,自动探测;后端一通立即加载
133
- - 后端关闭 / 被杀的一刻,窗口立刻切回离线页——不会停在旧页面假装还活着
134
- - 离线页内置快捷按钮:重新加载 / 启动后端 / 自动探测后端 / 设置后端安装文件夹
135
-
136
142
  ## 开发
137
143
 
138
144
  ```sh
@@ -144,6 +150,12 @@ npm run pack # 打包 NSIS (Win) / DMG (mac)
144
150
 
145
151
  ## 更新历史
146
152
 
153
+ ### 0.1.4
154
+ - 分支二(插件市场分发)正式可用:`dsh plugin add` 装插件 → 重启 `dsh web` → 桌面壳自动弹出;Electron 运行时由插件自管理(本地复用 / 按网络环境自动选源下载)。
155
+ - 桌面快捷方式:首次启动询问创建 + 托盘「创建桌面快捷方式」一键添加(安装包与插件两种形态均支持)。
156
+ - 图标:Windows 任务栏 / macOS Dock 均显示鲸鱼图标(裸运行时场景)。
157
+ - 移除开机自启(两种形态统一为纯手动启动)。
158
+
147
159
  ### 0.1.2
148
160
  - Windows 自动更新:托盘「检查更新」改为后台下载 + 进度显示 + 重启安装(electron-updater);macOS 仍为手动下载。
149
161
  - 移除启动时自动拉起后端(改纯手动,与「关闭后端」不冲突)。
package/build/icon.ico ADDED
Binary file
@@ -0,0 +1,17 @@
1
+ import { app } from 'electron'
2
+
3
+ /**
4
+ * AppUserModelID, shared by main (process-level), window (taskbar button)
5
+ * and shortcut (Start-menu .lnk) so the three always agree.
6
+ *
7
+ * Why plugin mode gets its own ID: Windows caches the taskbar button icon
8
+ * per AUMID. If the plugin shell reuses the installer's ID that ever ran
9
+ * with Electron's default icon (an early dev launch / an old build), the
10
+ * taskbar keeps showing that stale icon — no window `icon`, `setAppDetails`
11
+ * or exe patch can override the cache. A distinct ID makes Windows treat it
12
+ * as a brand-new app and re-read the icon. It also keeps the plugin shell
13
+ * from merging with an installed copy on the same machine.
14
+ */
15
+ export const APP_USER_MODEL_ID = app.isPackaged
16
+ ? 'com.icather.dsh-clean-desktop-shell'
17
+ : 'com.icather.dsh-clean-desktop-shell.plugin'
package/electron/main.js CHANGED
@@ -8,20 +8,39 @@
8
8
  *
9
9
  * The main window is a pure shell — all backend controls live in the tray.
10
10
  */
11
- import { app, BrowserWindow, dialog } from 'electron'
11
+ import { app, BrowserWindow, dialog, nativeImage } from 'electron'
12
+ import { fileURLToPath } from 'node:url'
12
13
  import { createMainWindow, reloadWindow } from './window.js'
13
14
  import { createTray, refreshTrayMenu } from './tray.js'
14
15
  import { loadConfig, saveConfig } from './config.js'
15
16
  import { detect } from './service.js'
16
17
  import { setupAutoUpdater } from './update.js'
17
- import { shortcutSupported, hasDesktopShortcut, createDesktopShortcut } from './shortcut.js'
18
+ import { shortcutSupported, hasDesktopShortcut, createDesktopShortcut, ensureStartMenuShortcut } from './shortcut.js'
19
+ import { APP_USER_MODEL_ID } from './aumid.js'
18
20
 
19
21
  const isMac = process.platform === 'darwin'
20
22
 
21
23
  // Windows: pin the AppUserModelId so the taskbar shows our whale icon
22
- // instead of the generic Electron icon.
24
+ // instead of the generic Electron icon. Plugin mode uses a distinct ID
25
+ // (see aumid.js) so Windows re-reads the icon instead of serving a
26
+ // stale per-AUMID cached one.
23
27
  if (process.platform === 'win32') {
24
- app.setAppUserModelId('com.icather.dsh-clean-desktop-shell')
28
+ app.setAppUserModelId(APP_USER_MODEL_ID)
29
+ }
30
+
31
+ // macOS: a bare runtime has no .app bundle (no icon resource), so set the
32
+ // Dock icon at runtime. Unlike Windows there is no per-AUMID taskbar cache
33
+ // here — app.dock.setIcon applies directly. Packaged builds already carry
34
+ // the icon in their bundle, so skip those.
35
+ if (isMac && !app.isPackaged) {
36
+ try {
37
+ const dockIcon = nativeImage.createFromPath(
38
+ fileURLToPath(new URL('../build/icon.png', import.meta.url)),
39
+ )
40
+ if (!dockIcon.isEmpty()) app.dock.setIcon(dockIcon)
41
+ } catch {
42
+ // non-fatal: keep the default icon
43
+ }
25
44
  }
26
45
 
27
46
  // Uniform userData across both distribution branches (installer vs
@@ -130,6 +149,14 @@ if (!gotLock) {
130
149
  // Windows: wire the auto-updater (downloads new installers silently).
131
150
  setupAutoUpdater()
132
151
 
152
+ // Windows: ensure the AUMID-carrying Start-menu shortcut exists so the
153
+ // taskbar button shows our icon (see shortcut.js). Best-effort.
154
+ try {
155
+ ensureStartMenuShortcut()
156
+ } catch {
157
+ // non-fatal
158
+ }
159
+
133
160
  // First run: offer a desktop shortcut (never nags twice).
134
161
  ensureShortcut().catch(() => {})
135
162
 
@@ -5,20 +5,44 @@
5
5
  * - first-run prompt in main.js ("create a desktop shortcut?")
6
6
  * - the tray "create desktop shortcut" item (always available)
7
7
  *
8
- * Only meaningful for packaged apps — in dev mode there is no stable
9
- * executable to point the shortcut at, so the feature is disabled there.
8
+ * Works in both distribution branches:
9
+ * - installer (packaged): targets the installed exe, no arguments
10
+ * - plugin-market (bare runtime): targets the provisioned electron.exe
11
+ * with the plugin's electron/main.js as its argument
12
+ *
13
+ * Pure dev mode (npm run dev from a checkout) has no stable executable,
14
+ * so the feature stays disabled there.
10
15
  */
11
- import { app } from 'electron'
16
+ import { app, shell } from 'electron'
12
17
  import { spawn } from 'node:child_process'
18
+ import { existsSync } from 'node:fs'
13
19
  import { homedir } from 'node:os'
14
20
  import { dirname, join } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { APP_USER_MODEL_ID } from './aumid.js'
15
23
 
16
24
  const isWin = process.platform === 'win32'
17
25
  const SHORTCUT_NAME = 'DSH Clean Desktop Shell.lnk'
26
+ const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
27
+ const ICON_ICO = join(PKG_ROOT, 'build', 'icon.ico')
28
+
29
+ /**
30
+ * Plugin-market mode: the host half spawns a bare electron runtime with the
31
+ * plugin's electron/main.js as its entry. process.execPath is the runtime
32
+ * electron.exe; process.argv[1] is the entry script (packaged installers
33
+ * have no such argument).
34
+ */
35
+ function pluginArgs() {
36
+ if (app.isPackaged) return []
37
+ const entry = process.argv[1]
38
+ if (entry && /\.js$/i.test(entry)) return [entry]
39
+ return []
40
+ }
18
41
 
19
- /** Shortcuts are a Windows packaged-app feature. */
42
+ /** Shortcuts need a stable executable — supported when packaged or in
43
+ * plugin mode (where the runtime path is fixed under DSH_HOME). */
20
44
  export function shortcutSupported() {
21
- return isWin && app.isPackaged
45
+ return isWin && (app.isPackaged || pluginArgs().length > 0)
22
46
  }
23
47
 
24
48
  function ps(str) {
@@ -59,14 +83,48 @@ export async function hasDesktopShortcut() {
59
83
  export async function createDesktopShortcut() {
60
84
  if (!shortcutSupported()) return false
61
85
  const target = process.execPath
86
+ const args = pluginArgs()
62
87
  const lnk = join(homedir(), 'Desktop', SHORTCUT_NAME)
88
+ // Plugin mode: point the shortcut at the runtime exe + main.js argument.
89
+ // WScript needs the path double-quoted inside the Arguments string.
90
+ const argPart = args.length
91
+ ? `; $s.Arguments = ${ps(`"${args[0]}"`)}; `
92
+ : ''
93
+ // Plugin-mode exe has no custom icon resource — point the shortcut at the
94
+ // bundled .ico when present (packaged installers use the exe itself).
95
+ const iconLoc = existsSync(ICON_ICO) ? ICON_ICO : `${target},0`
63
96
  const script =
64
97
  `$ws = New-Object -ComObject WScript.Shell; ` +
65
98
  `$s = $ws.CreateShortcut(${ps(lnk)}); ` +
66
99
  `$s.TargetPath = ${ps(target)}; ` +
100
+ argPart +
67
101
  `$s.WorkingDirectory = ${ps(dirname(target))}; ` +
68
- `$s.IconLocation = ${ps(`${target},0`)}; ` +
102
+ `$s.IconLocation = ${ps(iconLoc)}; ` +
69
103
  `$s.Save()`
70
104
  const out = await runPs(script)
71
105
  return out !== null
72
106
  }
107
+
108
+ /**
109
+ * Ensure a Start-menu shortcut carrying the AppUserModelID. This is what
110
+ * makes the taskbar button show our icon: Windows matches a running
111
+ * window's AUMID to a shortcut's icon (bare runtime electron.exe has no
112
+ * icon resource of its own). Idempotent — the shortcut is only created
113
+ * when missing.
114
+ */
115
+ export function ensureStartMenuShortcut() {
116
+ if (!isWin || !shortcutSupported()) return false
117
+ const lnkDir = join(
118
+ process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'),
119
+ 'Microsoft', 'Windows', 'Start Menu', 'Programs',
120
+ )
121
+ const lnk = join(lnkDir, SHORTCUT_NAME)
122
+ const args = pluginArgs()
123
+ return shell.writeShortcutLink(lnk, 'create', {
124
+ target: process.execPath,
125
+ args: args.length ? args.join(' ') : undefined,
126
+ icon: existsSync(ICON_ICO) ? ICON_ICO : undefined,
127
+ iconIndex: 0,
128
+ appUserModelId: APP_USER_MODEL_ID,
129
+ })
130
+ }
@@ -15,16 +15,26 @@
15
15
  * window flips back to the offline screen instead of showing a stale page
16
16
  * that suggests the app is still alive.
17
17
  */
18
- import { BrowserWindow, ipcMain } from 'electron'
18
+ import { app, BrowserWindow, ipcMain } from 'electron'
19
+ import { existsSync } from 'node:fs'
20
+ import { dirname, join } from 'node:path'
19
21
  import { fileURLToPath } from 'node:url'
20
22
  import { probe, onStatusChange, detect } from './service.js'
21
23
  import { startBackendWithProgress, chooseBackendFolder } from './tray.js'
24
+ import { APP_USER_MODEL_ID } from './aumid.js'
22
25
 
23
26
  export const WINDOWS_TITLEBAR_HEIGHT = 32
24
27
 
28
+ const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
25
29
  const PRELOAD_PATH = fileURLToPath(new URL('./preload.js', import.meta.url))
30
+ // Windows taskbar follows the window icon only when it is an .ico; a png
31
+ // covers the title bar / alt-tab but not the taskbar button. In plugin
32
+ // mode there is no exe icon resource, so prefer the bundled .ico.
33
+ const TASKBAR_ICO = join(PKG_ROOT, 'build', 'icon.ico')
26
34
  // Black-whale app icon (matches the DSH web favicon).
27
- const ICON_PATH = fileURLToPath(new URL('../build/icon.png', import.meta.url))
35
+ const ICON_PATH = existsSync(TASKBAR_ICO)
36
+ ? TASKBAR_ICO
37
+ : fileURLToPath(new URL('../build/icon.png', import.meta.url))
28
38
  // Local fallback page shown while the backend is down.
29
39
  const ERROR_PAGE = fileURLToPath(new URL('./error.html', import.meta.url))
30
40
 
@@ -189,6 +199,15 @@ export function createMainWindow({ target }) {
189
199
  // Linux / other: keep the native frame.
190
200
 
191
201
  const win = new BrowserWindow(options)
202
+ // Windows taskbar button: bare runtime electron.exe has no custom icon,
203
+ // so pin the button to our .ico via setAppDetails (appId must match the
204
+ // app-level AppUserModelId set in main.js, else the options are ignored).
205
+ if (process.platform === 'win32' && existsSync(TASKBAR_ICO)) {
206
+ win.setAppDetails({
207
+ appId: APP_USER_MODEL_ID,
208
+ appIconPath: TASKBAR_ICO,
209
+ })
210
+ }
192
211
  windowTargets.set(win.id, target)
193
212
  win.loadURL(target).catch(() => startReconnect(win, target))
194
213
 
package/lib/client.js CHANGED
@@ -1,10 +1,19 @@
1
1
  /**
2
2
  * dsh-clean-desktop-shell — client half (web browser bundle).
3
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.
4
+ * The shell is a standalone Electron window; it injects nothing into the
5
+ * dsh web UI. This client module exists only to satisfy the client-modules
6
+ * loader contract a bundle's client entry must register itself via
7
+ * `window.__ModuleLoader__.load({ id, factory })`, otherwise dsh reports
8
+ * "loaded without registering … via __ModuleLoader__.load".
7
9
  */
8
- export function apply() {
9
- // placeholder: settings-row wiring arrives with the shell implementation
10
- }
10
+ window.__ModuleLoader__.load({
11
+ id: 'dsh-clean-desktop-shell',
12
+ factory: () => {
13
+ var module = { exports: {} };
14
+ var exports = module.exports;
15
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
16
+ exports.apply = () => {};
17
+ return module.exports;
18
+ },
19
+ });
package/lib/common.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Shared constants and helpers for the host half modules.
3
+ *
4
+ * These compiled files sit at lib/<name>.js, so two dirname hops reach the
5
+ * package root — the same layout as src/host/ before build, and the same
6
+ * location electron/ and build/ live in the published package.
7
+ */
8
+ import { homedir } from 'node:os'
9
+ import { dirname, join } from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+
12
+ export const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
13
+ export const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
14
+ export const isWin = process.platform === 'win32'
15
+ export const EXE_NAME = isWin ? 'electron.exe' : 'electron'
16
+ export const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
17
+ export const PLATFORM = isWin ? 'win32' : process.platform === 'darwin' ? 'darwin' : 'linux'
18
+
19
+ /** Where the self-provisioned runtimes live (shared with icon.js). */
20
+ export function runtimeRoot() {
21
+ return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'desktop-shell-runtime')
22
+ }
package/lib/icon.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Patch the runtime electron.exe's icon resource so the Windows taskbar
3
+ * shows our whale icon.
4
+ *
5
+ * A bare runtime exe ships Electron's default icon and — as documented —
6
+ * no runtime API (BrowserWindow icon, setAppDetails, AUMID shortcuts) can
7
+ * change the taskbar button: it reads the exe's icon resource. rcedit
8
+ * (electron team's official tool) rewrites it in place.
9
+ *
10
+ * Best-effort: icon patching must never block the shell from launching.
11
+ * Idempotent: a marker file next to the exe records success; a re-provisioned
12
+ * (new version) exe has no marker and gets patched again.
13
+ */
14
+ import { spawn } from 'node:child_process'
15
+ import { existsSync, writeFileSync } from 'node:fs'
16
+ import { join } from 'node:path'
17
+ import { PKG_ROOT, isWin, runtimeRoot } from './common.js'
18
+
19
+ export async function patchExeIcon(ctx, exe) {
20
+ if (!isWin) return
21
+ const ico = join(PKG_ROOT, 'build', 'icon.ico')
22
+ if (!existsSync(ico)) return
23
+ const marker = `${exe}.whale-icon`
24
+ if (existsSync(marker)) return
25
+
26
+ // rcedit is a single self-contained exe, cached next to the runtimes.
27
+ const rcedit = join(runtimeRoot(), 'rcedit-x64.exe')
28
+ if (!existsSync(rcedit)) {
29
+ const url = 'https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe'
30
+ ctx.logger.info('[clean-desktop-shell] downloading rcedit for icon patching')
31
+ if (!(await fetchFile(url, rcedit))) {
32
+ ctx.logger.warn('[clean-desktop-shell] rcedit download failed — taskbar icon stays default')
33
+ return
34
+ }
35
+ }
36
+
37
+ const child = spawn(rcedit, [exe, '--set-icon', ico], {
38
+ windowsHide: true,
39
+ stdio: 'ignore',
40
+ })
41
+ const ok = await new Promise((resolve) => {
42
+ child.on('error', () => resolve(false))
43
+ child.on('exit', (code) => resolve(code === 0))
44
+ })
45
+ if (ok) {
46
+ writeFileSync(marker, String(Date.now()), 'utf8')
47
+ ctx.logger.info('[clean-desktop-shell] taskbar icon patched (rcedit)')
48
+ } else {
49
+ ctx.logger.warn('[clean-desktop-shell] rcedit patch failed — taskbar icon stays default')
50
+ }
51
+ }
52
+
53
+ function fetchFile(url, dest) {
54
+ return new Promise((resolve) => {
55
+ const child = spawn(
56
+ 'curl',
57
+ ['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
58
+ { windowsHide: true, stdio: 'ignore' },
59
+ )
60
+ child.on('error', () => resolve(false))
61
+ child.on('exit', (code) => resolve(code === 0))
62
+ })
63
+ }