@herenfor/dsh-wsl 0.1.0 → 0.1.1

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.md CHANGED
@@ -26,7 +26,7 @@ Lets DeepSeek Harness Desktop on Windows run tasks in WSL. For a task set to WSL
26
26
  ## Install
27
27
 
28
28
  1. Desktop → Plugins → Add plugin → Install a third-party plugin, enter `@herenfor/dsh-wsl` and install.
29
- 2. Quit Desktop completely, wait at least 5 seconds, then open it again.
29
+ 2. Quit Desktop completely (if it keeps running in the system tray, quit it there too), wait at least 5 seconds, then open it again. Until then the plugin does not start working, and the bottom of the sidebar says to restart Desktop.
30
30
  3. On the new-task page, switch the run location under the input box to WSL. The first time, "Set up the WSL environment" opens; choose one way:
31
31
  - **Reuse your dsh**: offered when WSL already has dsh. Desktop shares data with the dsh you use in WSL yourself, so tasks show up on both; updates update both.
32
32
  - **A separate copy for this plugin**: installed in `~/.dsh-wsl/dsh` with its data in `~/.dsh-wsl/home`, leaving your own dsh alone. About 250 MB to download, about 900 MB installed.
@@ -34,6 +34,13 @@ Lets DeepSeek Harness Desktop on Windows run tasks in WSL. For a task set to WSL
34
34
  If Node.js or DSH is missing, the window lists the commands to run. Click "Install automatically", click "Let DSH install it" to have the Windows DSH run them, or run them yourself in a WSL terminal. The plugin never downloads anything in the background on its own.
35
35
  4. Once connected, the loading note at the bottom of the sidebar goes away and the WSL folders and tasks appear in the sidebar.
36
36
 
37
+ ### Other ways to install
38
+
39
+ The install box in step 1 also takes these, but a plugin installed this way does not update itself:
40
+
41
+ - From GitHub: `github:herenfor/dsh-wsl#path:/experiments/mirror/mirror`, or for a given version `github:herenfor/dsh-wsl#v0.1.0&path:/experiments/mirror/mirror`.
42
+ - From a local folder: download the repository to Windows and enter the full path of its `experiments/mirror/mirror` folder.
43
+
37
44
  ## Installing plugins in WSL
38
45
 
39
46
  Plugins → "WSL (distribution)" → enter an npm package name, git URL or local folder path in the install box → Check → Install. Windows paths are turned into WSL paths. When it says a restart is needed, quit Desktop completely, wait at least 5 seconds and open it again.
package/README.zh.md CHANGED
@@ -26,7 +26,7 @@
26
26
  ## 安装
27
27
 
28
28
  1. Desktop → 插件 → 添加插件 → 安装第三方插件,填 `@herenfor/dsh-wsl`,然后安装。
29
- 2. 完全退出 Desktop,等 5 秒以上再打开。
29
+ 2. 完全退出 Desktop(如果它还在系统托盘里运行,也要在托盘里退出),等 5 秒以上再打开。不重启的话插件不会开始工作,侧栏底部会提示「重启 Desktop 后生效」。
30
30
  3. 在新任务页,把输入框下方的运行位置切到 WSL。第一次会弹出「设置 WSL 运行环境」,选一种方式:
31
31
  - **复用你装的 dsh**:WSL 里已经装了 dsh 时可以选。Desktop 和你在 WSL 里自己用的 dsh 共用数据,任务互相看得到;更新时两边一起更新。
32
32
  - **插件专用一份**:装在 `~/.dsh-wsl/dsh`,数据放在 `~/.dsh-wsl/home`,不影响你自己的 dsh。要下载约 250 MB,装好后约占 900 MB。
@@ -34,6 +34,13 @@
34
34
  缺 Node.js 或 DSH 时,窗口里会列出要运行的命令。你可以点「自动安装」,点「交给 DSH 安装」让 Windows 端的 DSH 去执行,或者自己在 WSL 终端里运行。插件不会自己在后台下载。
35
35
  4. 连上之后,侧栏底部的加载提示会消失,WSL 的文件夹和任务出现在侧栏里。
36
36
 
37
+ ### 其他安装方式
38
+
39
+ 第 1 步的安装框也接受下面两种写法,但这样装的不会自动更新:
40
+
41
+ - 从 GitHub 安装:`github:herenfor/dsh-wsl#path:/experiments/mirror/mirror`。指定版本时写成 `github:herenfor/dsh-wsl#v0.1.0&path:/experiments/mirror/mirror`。
42
+ - 从本地文件夹安装:把仓库下载到 Windows 上,填 `experiments/mirror/mirror` 文件夹的完整路径。
43
+
37
44
  ## 在 WSL 里装插件
38
45
 
39
46
  插件 →「WSL(发行版名)」→ 在安装框里填 npm 包名、git 地址或本地文件夹路径 → 检查 → 安装。Windows 路径会自动换成 WSL 路径。提示需要重启的,完全退出 Desktop,等 5 秒以上再打开。
package/client.js CHANGED
@@ -22,7 +22,8 @@ window.__ModuleLoader__.load({
22
22
  wslFolderNote: '浏览 Linux 文件系统;任务在 WSL 中运行,读写更快',
23
23
  setupNoWsl: '这台电脑没有安装 WSL', setupNoDistro: 'WSL 里还没有 Linux 发行版', setupWsl1: '这个发行版是 WSL 1,需要 WSL 2', setupChoose: '还没有选择在 WSL 里怎么运行 DSH',
24
24
  setupNeedsDsh: 'WSL 里还没有装 DSH', setupVersionMismatch: 'WSL 里的 DSH 版本较旧,需要更新', setupDesktopOutdated: 'Desktop 版本较旧,需要先升级 Desktop',
25
- setupInstalling: '正在安装 WSL 端的 DSH…', setupChecking: '正在检查 WSL…', setupStarting: '正在启动 WSL 端…', setupError: 'WSL 端启动失败', setupConnected: '已连接',
25
+ setupInstalling: '正在安装 WSL 端的 DSH…', setupChecking: '正在检查 WSL…', setupStarting: '正在启动 WSL 端…', setupError: 'WSL 端启动失败', setupConnected: '已连接', setupRestart: '完全退出并重新打开 Desktop 后生效',
26
+ statusRestart: 'WSL 运行模式:重启 Desktop 后生效', restartBody: 'WSL 运行模式是在 Desktop 运行时装上(或打开)的,要完全退出并重新打开 Desktop 后才会开始工作。', restartNote: '关闭窗口后,如果 Desktop 还在系统托盘里运行,也要在托盘里退出。等 5 秒以上再打开。',
26
27
  copy: '复制', copied: '已复制', current: '当前',
27
28
  modeAskOwn: 'WSL 里已经装了 DSH {version}。Desktop 在 WSL 里运行任务时,用哪一份?', modeAsk: 'Desktop 在 WSL 里运行任务时,用哪一份 DSH?',
28
29
  reuseTitle: '复用你装的 dsh({version})', reuseSync: '会话同步:Desktop 里在 WSL 运行的任务,和你在 WSL 里自己用 dsh 时是同一份数据,任务记录、文件夹列表、插件互相都看得到。',
@@ -73,7 +74,8 @@ window.__ModuleLoader__.load({
73
74
  wslFolderNote: 'Browse the Linux file system; tasks run in WSL with faster file access',
74
75
  setupNoWsl: 'WSL is not installed on this computer', setupNoDistro: 'WSL has no Linux distribution yet', setupWsl1: 'This distribution runs on WSL 1; WSL 2 is needed', setupChoose: 'How DSH runs in WSL is not chosen yet',
75
76
  setupNeedsDsh: 'DSH is not installed in WSL yet', setupVersionMismatch: 'The DSH in WSL is too old and needs an update', setupDesktopOutdated: 'Desktop is too old; update Desktop first',
76
- setupInstalling: 'Installing DSH in WSL…', setupChecking: 'Checking WSL…', setupStarting: 'Starting the WSL side…', setupError: 'The WSL side failed to start', setupConnected: 'Connected',
77
+ setupInstalling: 'Installing DSH in WSL…', setupChecking: 'Checking WSL…', setupStarting: 'Starting the WSL side…', setupError: 'The WSL side failed to start', setupConnected: 'Connected', setupRestart: 'quit Desktop completely and open it again to start using it',
78
+ statusRestart: 'WSL run mode: restart Desktop to start', restartBody: 'WSL run mode was installed (or switched on) while Desktop was running. It starts working once you quit Desktop completely and open it again.', restartNote: 'If Desktop keeps running in the system tray after you close its window, quit it there too. Wait at least 5 seconds before opening it again.',
77
79
  copy: 'Copy', copied: 'Copied', current: 'Current',
78
80
  modeAskOwn: 'DSH {version} is already installed in WSL. Which copy should Desktop use to run tasks in WSL?', modeAsk: 'Which DSH should Desktop use to run tasks in WSL?',
79
81
  reuseTitle: 'Reuse your dsh ({version})', reuseSync: 'Sessions shared: tasks Desktop runs in WSL use the same data as the dsh you run in WSL yourself, so tasks, folders and plugins show up on both.',
@@ -229,7 +231,10 @@ window.__ModuleLoader__.load({
229
231
  return () => { document.removeEventListener('mousedown', close, true); document.removeEventListener('keydown', esc, true) }
230
232
  }, [open, host])
231
233
  const placeholder = h('span', { ref: anchor, style: { display: 'none' } })
232
- if (!ReactDOM?.createPortal || !host || !workspaceId || state === null || state.workspaceId !== workspaceId) return placeholder
234
+ const shown = !!ReactDOM?.createPortal && !!host && !!workspaceId && state !== null && state.workspaceId === workspaceId
235
+ // The container under the composer takes no room while there is nothing to show.
236
+ React.useEffect(() => { if (host) host.style.display = shown ? 'flex' : 'none' }, [host, shown])
237
+ if (!shown) return placeholder
233
238
  const wsl = state.target === 'wsl'
234
239
  const distro = state.distro ?? 'WSL'
235
240
  const choose = async (target) => {
@@ -325,7 +330,7 @@ window.__ModuleLoader__.load({
325
330
  const SETUP_KEYS = {
326
331
  'no-wsl': 'setupNoWsl', 'no-distro': 'setupNoDistro', wsl1: 'setupWsl1', choose: 'setupChoose', 'needs-node': 'setupNeedsDsh', 'needs-dsh': 'setupNeedsDsh',
327
332
  'version-mismatch': 'setupVersionMismatch', 'desktop-outdated': 'setupDesktopOutdated', installing: 'setupInstalling', checking: 'setupChecking',
328
- starting: 'setupStarting', error: 'setupError', connected: 'setupConnected',
333
+ starting: 'setupStarting', error: 'setupError', connected: 'setupConnected', 'restart-required': 'setupRestart',
329
334
  }
330
335
  /** One line on a setup state, or undefined for a state without one. */
331
336
  const setupNote = (state) => (SETUP_KEYS[state] ? t(SETUP_KEYS[state]) : undefined)
@@ -431,6 +436,7 @@ window.__ModuleLoader__.load({
431
436
  s.probeError ? h(CodeBlock, { text: s.probeError }) : null,
432
437
  s.log ? note(t('logAt'), code(s.log), t('logAtEnd')) : null,
433
438
  ]
439
+ else if (s.state === 'restart-required') body = [para(t('restartBody')), note(t('restartNote'))]
434
440
  else if (s.state === 'starting' || s.state === 'checking') body = [para(t('setupStarting')), note(t('startingNote'))]
435
441
  else if (s.state === 'connected') {
436
442
  body = [para(t('connectedBody', { distro, version: s.wslVersion ?? '' }))]
@@ -559,13 +565,15 @@ window.__ModuleLoader__.load({
559
565
  else if (LOADING_STATES.has(setup.state)) text = t('statusStarting', { distro })
560
566
  else if (connected && !listed && !settled) text = t('statusLoading')
561
567
  else if (setup.state === 'error') text = t('statusFailed', { distro })
568
+ else if (setup.state === 'restart-required') text = t('statusRestart')
562
569
  const modal = dialog ? h(SetupDialog, { key: 'setup', ui, onClose: () => setDialog(false) }) : null
563
570
  if (text === undefined) return modal
564
571
  const failed = setup.state === 'error'
572
+ const waiting = !failed && setup.state !== 'restart-required'
565
573
  return h(React.Fragment, null, h('button', {
566
574
  type: 'button', title: text, onClick: () => setDialog(true), ...hoverable(),
567
575
  style: { display: 'flex', alignItems: 'center', justifyContent: wide ? 'flex-start' : 'center', gap: 8, width: '100%', boxSizing: 'border-box', padding: wide ? '8px 12px' : '8px 0', border: 0, borderRadius: 'var(--dsw-radius-md, 8px)', background: 'transparent', color: failed ? 'var(--dsw-alias-state-error-primary, #e5484d)' : MUTED, font: 'inherit', fontSize: 13, textAlign: 'left', cursor: 'pointer' },
568
- }, failed ? h(PenguinIcon) : h(Spinner), wide ? h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, text) : null), modal)
576
+ }, waiting ? h(Spinner) : h(PenguinIcon), wide ? h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, text) : null), modal)
569
577
  }
570
578
 
571
579
  // ---- the WSL tab of the Plugins page: the WSL side's plugins, apart from the official Installed tab (Windows').
package/index.mjs CHANGED
@@ -48,7 +48,35 @@ function readBridgeConfig(config) {
48
48
  }
49
49
  const readJson = (file) => { try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return {} } }
50
50
 
51
+ // Installed or switched on while Desktop runs: the page and its streams were opened before this plugin could route
52
+ // them, so it would half work (WSL folders without their tasks, a composer that stays grey). Such a load takes over
53
+ // nothing and asks for a restart instead. Only the first load in the process decides: a plugin that came up with the
54
+ // Desktop and is loaded again later (a service it uses restarted) works as before.
55
+ const HOT_LOAD_SECONDS = Number(process.env.DSH_WSL_HOT_LOAD_SECONDS ?? 60)
56
+ function loadedWhileRunning() {
57
+ const seen = globalThis.__dshWslLoad
58
+ if (seen === undefined) globalThis.__dshWslLoad = process.uptime() > HOT_LOAD_SECONDS ? 'restart-required' : 'boot'
59
+ return globalThis.__dshWslLoad === 'restart-required'
60
+ }
61
+
62
+ /** The plugin when it needs a Desktop restart: its routes say so and nothing else is touched (no WSL, no routing). */
63
+ function applyRestartRequired(ctx) {
64
+ ctx.logger.info('wsl-mirror: loaded while Desktop was running; it starts working after Desktop restarts')
65
+ const json = (res, value) => { res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value)) }
66
+ const route = (path, value) => ctx.effect(() => ctx.webServer.register({
67
+ kind: 'exact', path,
68
+ handler(req, res) {
69
+ const admission = ctx.connection.admit(req)
70
+ if ('rejection' in admission) { res.writeHead(admission.rejection); res.end(); return }
71
+ json(res, value)
72
+ },
73
+ }), `wsl-mirror: ${path} (restart required)`)
74
+ route('/wsl-bridge/setup', { state: 'restart-required', distro: null, distros: [], dev: false, mode: null, own: null, commands: '', prompt: '', canInstall: false, plugin: null, job: null })
75
+ route('/wsl-bridge/plugins', { connected: false, distro: null, setup: 'restart-required' })
76
+ }
77
+
51
78
  export async function apply(ctx, config = {}) {
79
+ if (loadedWhileRunning()) return applyRestartRequired(ctx)
52
80
  const bridgeConfig = readBridgeConfig(config ?? {})
53
81
  // First-run setup (setup.mjs): the state of the WSL side, asked again before every connect and on "re-check".
54
82
  let setup = { state: 'checking' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herenfor/dsh-wsl",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "WSL run mode for DeepSeek Harness Desktop on Windows: run chosen tasks, and the plugins they use, in WSL from the Desktop window",
5
5
  "license": "MIT",
6
6
  "icon": "./icon.svg",
package/routing.mjs CHANGED
@@ -117,7 +117,8 @@ export function installSessionRouting(ctx, { bridge, remoteOrigin, cookie, forwa
117
117
  const local = (changed) => { replay(req, changed === undefined ? body : Buffer.from(JSON.stringify({ ...message, payload: { ...payload, args: changed } }))); return next() }
118
118
 
119
119
  if (MERGED_LISTS.has(endpoint)) {
120
- const [mine, theirs] = await Promise.all([ownHost(req, pathname, body), rpc(endpoint, translate(args, false)).catch(() => undefined)])
120
+ const [mine, theirs] = await Promise.all([ownHost(req, pathname, body), rpc(endpoint, translate(args, false)).catch((error) => { debug('merged list', endpoint, 'WSL failed:', error.message); return undefined })])
121
+ debug('merged list', endpoint, 'local', mine?.result?.ok, mine?.result?.value?.items?.length, 'wsl', theirs?.ok, theirs?.value?.items?.length ?? theirs?.error?.code)
121
122
  if (mine?.result?.ok && theirs?.ok) mine.result.value = { ...mine.result.value, items: [...mine.result.value.items, ...localizeSessions(translate(theirs.value, true).items, paths())] }
122
123
  res.writeHead(200, { 'content-type': 'application/json' })
123
124
  res.end(JSON.stringify(mine))
@@ -342,6 +343,7 @@ export function installSessionRouting(ctx, { bridge, remoteOrigin, cookie, forwa
342
343
 
343
344
  /** One merged stream: this Host's (`native`, already open) and the WSL Host's, kept open with retries. */
344
345
  async function* merged(endpoint, payload, signal, native) {
346
+ debug('stream open', endpoint, 'wsl available', available())
345
347
  const queue = new Queue()
346
348
  const scope = new AbortController()
347
349
  const combined = AbortSignal.any([signal, scope.signal])
@@ -448,11 +450,14 @@ export function installSessionRouting(ctx, { bridge, remoteOrigin, cookie, forwa
448
450
  // The page lists Sessions once per connection (session/list); WSL may come after that (Desktop started
449
451
  // first, the person still setting WSL up, the WSL Host restarted). Its Sessions are announced to the page
450
452
  // as added, once per WSL connection, which the page merges by id.
453
+ debug('events: WSL ready', frame.clientId)
451
454
  void rpc('session/list', { _request: {} }).then((result) => {
455
+ debug('events: WSL session list', result?.ok, result?.value?.items?.length ?? result?.error?.code, 'aborted', signal.aborted)
452
456
  if (!result?.ok || signal.aborted) return
453
457
  for (const summary of localizeSessions(translate(result.value, true).items, paths())) queue.push({ source: 'wsl', value: { type: 'emit', event: 'api-session/added', args: [summary] }, listed: true })
454
458
  }).catch((error) => logger.debug('wsl-mirror: WSL session list: %s', error.message))
455
459
  }
460
+ if (item.source === 'local') debug('events: local ready', frame.clientId)
456
461
  if (localClient !== undefined && wslClient !== undefined) eventClients.set(localClient, wslClient)
457
462
  continue
458
463
  }