dsh-wsl-workspace 0.2.2 → 0.3.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/README.de.md +49 -0
- package/README.es.md +49 -0
- package/README.fr.md +49 -0
- package/README.ja.md +49 -0
- package/README.ko.md +49 -0
- package/README.md +47 -47
- package/README.pt.md +49 -0
- package/README.ru.md +49 -0
- package/README.zh.md +46 -46
- package/lib/client.js +76 -5
- package/lib/client.js.map +1 -1
- package/lib/fs.js +188 -174
- package/lib/fs.js.map +1 -1
- package/lib/index.js +573 -541
- package/lib/index.js.map +1 -1
- package/lib/{paths-DBaSmi7x.js → paths-BDE1NVOv.js} +17 -2
- package/lib/paths-BDE1NVOv.js.map +1 -0
- package/lib/shell.js +382 -381
- package/lib/shell.js.map +1 -1
- package/lib/wsl-C5_mxGPM.js +228 -0
- package/lib/wsl-C5_mxGPM.js.map +1 -0
- package/lib/wsl-DdNPGaPo.js +1019 -0
- package/lib/wsl-DdNPGaPo.js.map +1 -0
- package/lib/wsl-credentials-BI4v5TNZ.js +109 -0
- package/lib/wsl-credentials-BI4v5TNZ.js.map +1 -0
- package/package.json +71 -61
- package/src/client/AddWslWorkspace.tsx +349 -346
- package/src/client/api.ts +124 -104
- package/src/client/index.ts +233 -194
- package/src/fs.ts +247 -228
- package/src/host/variants.ts +164 -199
- package/src/index.ts +93 -27
- package/src/shared/paths.ts +175 -159
- package/src/shared/wsl-credentials.ts +135 -85
- package/src/shell.ts +451 -441
- package/image-1.png +0 -0
- package/image.png +0 -0
- package/lib/paths-DBaSmi7x.js.map +0 -1
- package/lib/wsl-GjkUifnx.js +0 -179
- package/lib/wsl-GjkUifnx.js.map +0 -1
package/src/client/index.ts
CHANGED
|
@@ -1,194 +1,233 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser half of dsh-wsl-workspace. Registers the "Add WSL workspace…"
|
|
3
|
-
* action beside Settings at the sidebar foot (the official
|
|
4
|
-
* `sidebar.footer.action` slot), and keeps every blank session whose
|
|
5
|
-
* workspace is a WSL UNC path composed from the WSL VARIANT of the mode it
|
|
6
|
-
* currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the
|
|
7
|
-
* WSL execution world composes with any mode instead of being a mode itself.
|
|
8
|
-
*
|
|
9
|
-
* The binding is a watching effect rather than a one-shot dialog action so
|
|
10
|
-
* EVERY creation path (this dialog, the workspace row's New Session, the
|
|
11
|
-
* hero picker) converges on the WSL-backed composition automatically.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
|
15
|
-
// Type-only: pulls the locale plugin's Context merge (ctx.locale), the
|
|
16
|
-
// runtime's ClientContext, and the ui-sidebar SlotMap merge (the
|
|
17
|
-
// 'sidebar.footer.action' entry) into this program.
|
|
18
|
-
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
-
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
20
|
-
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
21
|
-
import { check as checkApi, listDir as listDirApi, listDistros as listDistrosApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'
|
|
22
|
-
import { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'
|
|
23
|
-
import { ensureStyles } from './styles.ts'
|
|
24
|
-
import { zh, en } from './locales.ts'
|
|
25
|
-
import { isWslUnc } from '../shared/paths.ts'
|
|
26
|
-
|
|
27
|
-
/** Required services (cordis fiber inject). */
|
|
28
|
-
export const inject = ['slots', 'locale', 'connection', 'sessions', 'workspaces']
|
|
29
|
-
|
|
30
|
-
/** The legacy standalone WSL preset id (folded into the mode variants). */
|
|
31
|
-
const LEGACY_WSL_PRESET_ID = 'wsl'
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Minimal sessions-service face. The renderer-host ctx merge types
|
|
35
|
-
* `ctx.sessions` as its own SessionStore; the service the runtime actually
|
|
36
|
-
* registers under that key satisfies this narrower contract, so the cast is
|
|
37
|
-
* the documented boundary for a third-party plugin.
|
|
38
|
-
*/
|
|
39
|
-
interface WslSessionsFace {
|
|
40
|
-
list: {
|
|
41
|
-
getSnapshot(): { ids: string[]; byId: Record<string, { blank: boolean; cwd?: string; agentPreset?: string }> }
|
|
42
|
-
subscribe(fn: () => void): () => void
|
|
43
|
-
}
|
|
44
|
-
noteAgentPreset(sessionId: string, agentPreset: string): void
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Minimal workspaces-service face (create + start-session only). */
|
|
48
|
-
interface WslWorkspacesFace {
|
|
49
|
-
create(input: { path: string }): Promise<{ workspaceId: string }>
|
|
50
|
-
startSession(workspaceId?: string): void
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Mount the sidebar action and the auto-binding effect.
|
|
55
|
-
* @param ctx - the browser plugin context.
|
|
56
|
-
*/
|
|
57
|
-
export function apply(ctx: ClientContext): void {
|
|
58
|
-
const { api } = ctx.get('connection') as ConnectionHandle
|
|
59
|
-
const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace
|
|
60
|
-
const sessions = ctx.get('sessions') as unknown as WslSessionsFace
|
|
61
|
-
|
|
62
|
-
ensureStyles()
|
|
63
|
-
|
|
64
|
-
ctx.effect(
|
|
65
|
-
() => ctx.locale.register('wslWorkspace' as never, { zh, en }),
|
|
66
|
-
'dsh-wsl-workspace: locale dictionaries',
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// The injected translate function reads the live DeepSeek Harness locale,
|
|
70
|
-
// so the dialog copy follows the app language setting automatically.
|
|
71
|
-
const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of dsh-wsl-workspace. Registers the "Add WSL workspace…"
|
|
3
|
+
* action beside Settings at the sidebar foot (the official
|
|
4
|
+
* `sidebar.footer.action` slot), and keeps every blank session whose
|
|
5
|
+
* workspace is a WSL UNC path composed from the WSL VARIANT of the mode it
|
|
6
|
+
* currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the
|
|
7
|
+
* WSL execution world composes with any mode instead of being a mode itself.
|
|
8
|
+
*
|
|
9
|
+
* The binding is a watching effect rather than a one-shot dialog action so
|
|
10
|
+
* EVERY creation path (this dialog, the workspace row's New Session, the
|
|
11
|
+
* hero picker) converges on the WSL-backed composition automatically.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
|
|
15
|
+
// Type-only: pulls the locale plugin's Context merge (ctx.locale), the
|
|
16
|
+
// runtime's ClientContext, and the ui-sidebar SlotMap merge (the
|
|
17
|
+
// 'sidebar.footer.action' entry) into this program.
|
|
18
|
+
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
19
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
20
|
+
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
21
|
+
import { check as checkApi, listDir as listDirApi, listDistros as listDistrosApi, listWorkspaces as listWorkspacesApi, registerWindows as registerWindowsApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'
|
|
22
|
+
import { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'
|
|
23
|
+
import { ensureStyles } from './styles.ts'
|
|
24
|
+
import { zh, en } from './locales.ts'
|
|
25
|
+
import { canonicalWindowsPath, isWslUnc, joinUnc, mntToWindowsPath } from '../shared/paths.ts'
|
|
26
|
+
|
|
27
|
+
/** Required services (cordis fiber inject). */
|
|
28
|
+
export const inject = ['slots', 'locale', 'connection', 'sessions', 'workspaces']
|
|
29
|
+
|
|
30
|
+
/** The legacy standalone WSL preset id (folded into the mode variants). */
|
|
31
|
+
const LEGACY_WSL_PRESET_ID = 'wsl'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Minimal sessions-service face. The renderer-host ctx merge types
|
|
35
|
+
* `ctx.sessions` as its own SessionStore; the service the runtime actually
|
|
36
|
+
* registers under that key satisfies this narrower contract, so the cast is
|
|
37
|
+
* the documented boundary for a third-party plugin.
|
|
38
|
+
*/
|
|
39
|
+
interface WslSessionsFace {
|
|
40
|
+
list: {
|
|
41
|
+
getSnapshot(): { ids: string[]; byId: Record<string, { blank: boolean; cwd?: string; agentPreset?: string }> }
|
|
42
|
+
subscribe(fn: () => void): () => void
|
|
43
|
+
}
|
|
44
|
+
noteAgentPreset(sessionId: string, agentPreset: string): void
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Minimal workspaces-service face (create + start-session only). */
|
|
48
|
+
interface WslWorkspacesFace {
|
|
49
|
+
create(input: { path: string }): Promise<{ workspaceId: string }>
|
|
50
|
+
startSession(workspaceId?: string): void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mount the sidebar action and the auto-binding effect.
|
|
55
|
+
* @param ctx - the browser plugin context.
|
|
56
|
+
*/
|
|
57
|
+
export function apply(ctx: ClientContext): void {
|
|
58
|
+
const { api } = ctx.get('connection') as ConnectionHandle
|
|
59
|
+
const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace
|
|
60
|
+
const sessions = ctx.get('sessions') as unknown as WslSessionsFace
|
|
61
|
+
|
|
62
|
+
ensureStyles()
|
|
63
|
+
|
|
64
|
+
ctx.effect(
|
|
65
|
+
() => ctx.locale.register('wslWorkspace' as never, { zh, en }),
|
|
66
|
+
'dsh-wsl-workspace: locale dictionaries',
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// The injected translate function reads the live DeepSeek Harness locale,
|
|
70
|
+
// so the dialog copy follows the app language setting automatically.
|
|
71
|
+
const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string
|
|
72
|
+
|
|
73
|
+
// Canonical Windows drive keys of every registered `/mnt/<drive>` workspace
|
|
74
|
+
// (refreshed from the host store; see refreshRoster). A blank session whose
|
|
75
|
+
// cwd is one of these binds to the WSL variant like a UNC-cwd session.
|
|
76
|
+
let wslWindowsPaths = new Set<string>()
|
|
77
|
+
|
|
78
|
+
const injected = (): AddWslWorkspaceInjected => ({
|
|
79
|
+
t,
|
|
80
|
+
checkPreset: async (): Promise<string | undefined> => {
|
|
81
|
+
let roster
|
|
82
|
+
try {
|
|
83
|
+
const response = await api.agentPresets.list({})
|
|
84
|
+
roster = response.result
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return error instanceof Error ? error.message : String(error)
|
|
87
|
+
}
|
|
88
|
+
if (!roster.ok) return roster.error.message
|
|
89
|
+
const healthy = roster.value.presets.find((entry: { id: string; broken?: string }) =>
|
|
90
|
+
entry.id.startsWith('wsl-') && entry.broken === undefined)
|
|
91
|
+
if (healthy === undefined) return t('error.presetMissing')
|
|
92
|
+
return undefined
|
|
93
|
+
},
|
|
94
|
+
listDistros: () => listDistrosApi(),
|
|
95
|
+
listDir: (distro, path) => listDirApi(distro, path),
|
|
96
|
+
check: (distro, path) => checkApi(distro, path),
|
|
97
|
+
createWorkspace: async (linuxPath, username, distro): Promise<string | undefined> => {
|
|
98
|
+
try {
|
|
99
|
+
const winPath = mntToWindowsPath(linuxPath)
|
|
100
|
+
if (winPath !== null) {
|
|
101
|
+
// `/mnt/<drive>` workspace: the workspace registry realpath/stats
|
|
102
|
+
// the path and 9P cannot serve drvfs mounts, so register under the
|
|
103
|
+
// drive spelling and store distro/username for the session env.
|
|
104
|
+
// The browser binding below recognizes the drive cwd as WSL.
|
|
105
|
+
const view = await workspaces.create({ path: winPath })
|
|
106
|
+
await registerWindowsApi(linuxPath, distro, username)
|
|
107
|
+
const canonical = canonicalWindowsPath(winPath)
|
|
108
|
+
if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical)
|
|
109
|
+
workspaces.startSession(view.workspaceId)
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
const uncPath = joinUnc(distro, linuxPath)
|
|
113
|
+
const view = await workspaces.create({ path: uncPath })
|
|
114
|
+
await setWorkspaceUserApi(uncPath, username)
|
|
115
|
+
workspaces.startSession(view.workspaceId)
|
|
116
|
+
return undefined
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return error instanceof Error ? error.message : String(error)
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
ctx.effect(
|
|
124
|
+
() => ctx.slots.inject(
|
|
125
|
+
'sidebar.footer.action',
|
|
126
|
+
() => ctx.slots.register(
|
|
127
|
+
{ name: 'sidebar.footer.action', id: 'wsl-workspace', inject: injected },
|
|
128
|
+
AddWslWorkspace,
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
'dsh-wsl-workspace: sidebar footer action',
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
// Mode-variant binding: a blank session whose workspace is a WSL UNC path
|
|
135
|
+
// is recomposed to the WSL variant of the mode it currently runs — plain
|
|
136
|
+
// 标准 becomes `wsl-standard`, PTC becomes `wsl-code`, and so on — so the
|
|
137
|
+
// WSL execution world composes with ANY mode instead of replacing it. The
|
|
138
|
+
// host refuses non-blank sessions (agent-preset-locked), so the swap is
|
|
139
|
+
// attempted at most a few times per session.
|
|
140
|
+
ctx.effect(() => {
|
|
141
|
+
const inFlight = new Set<string>()
|
|
142
|
+
const attempts = new Map<string, number>()
|
|
143
|
+
const MAX_ATTEMPTS = 3
|
|
144
|
+
// Healthy `wsl-<mode>` variant ids plus the roster's default preset id
|
|
145
|
+
// (what a session with no explicit choice gets). Refreshed periodically
|
|
146
|
+
// so variants generated after this page loaded are picked up.
|
|
147
|
+
let variants = new Set<string>()
|
|
148
|
+
let defaultPreset: string | undefined
|
|
149
|
+
const refreshRoster = (): void => {
|
|
150
|
+
void api.agentPresets.list({}).then((response: {
|
|
151
|
+
result: { ok: boolean; value: { presets: { id: string; broken?: string; isDefault?: boolean }[] } }
|
|
152
|
+
}) => {
|
|
153
|
+
const result = response.result
|
|
154
|
+
if (!result.ok) return
|
|
155
|
+
variants = new Set(result.value.presets
|
|
156
|
+
.filter((entry: { id: string; broken?: string }) =>
|
|
157
|
+
entry.broken === undefined && entry.id.startsWith('wsl-'))
|
|
158
|
+
.map((entry: { id: string }) => entry.id))
|
|
159
|
+
defaultPreset = result.value.presets.find(
|
|
160
|
+
(entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,
|
|
161
|
+
)?.id
|
|
162
|
+
}).catch(() => {
|
|
163
|
+
// A failed roster read leaves the previous mapping; sessions stay on
|
|
164
|
+
// their current composition until the next refresh.
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
refreshRoster()
|
|
168
|
+
const refreshWorkspaces = (): void => {
|
|
169
|
+
void listWorkspacesApi().then((keys: string[]) => {
|
|
170
|
+
const next = new Set<string>()
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
const canonical = canonicalWindowsPath(key)
|
|
173
|
+
if (canonical !== null) next.add(canonical)
|
|
174
|
+
}
|
|
175
|
+
wslWindowsPaths = next
|
|
176
|
+
}).catch(() => {
|
|
177
|
+
// A failed store read leaves the previous set; sessions stay on
|
|
178
|
+
// their current composition until the next refresh.
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
refreshWorkspaces()
|
|
182
|
+
const maybeBind = (): void => {
|
|
183
|
+
const state = sessions.list.getSnapshot()
|
|
184
|
+
for (const id of state.ids) {
|
|
185
|
+
const summary = state.byId[id]
|
|
186
|
+
if (summary === undefined || !summary.blank || summary.cwd === undefined) continue
|
|
187
|
+
// A session belongs to the WSL world when its cwd is a WSL UNC path,
|
|
188
|
+
// or a Windows drive path registered as a `/mnt/<drive>` workspace
|
|
189
|
+
// (9P cannot serve drvfs, so those workspaces carry drive cwds).
|
|
190
|
+
const canonical = canonicalWindowsPath(summary.cwd)
|
|
191
|
+
const isWsl = isWslUnc(summary.cwd)
|
|
192
|
+
|| (canonical !== null && wslWindowsPaths.has(canonical))
|
|
193
|
+
if (!isWsl) continue
|
|
194
|
+
const current = summary.agentPreset
|
|
195
|
+
if (current !== undefined && current.startsWith('wsl-')) continue
|
|
196
|
+
// Legacy standalone `wsl` (now folded into the variants): remap it to
|
|
197
|
+
// the default mode's variant, since the standalone preset no longer
|
|
198
|
+
// exists in the roster.
|
|
199
|
+
const base = current === LEGACY_WSL_PRESET_ID
|
|
200
|
+
? (defaultPreset ?? 'standard')
|
|
201
|
+
: (current ?? defaultPreset)
|
|
202
|
+
if (base === undefined || base === LEGACY_WSL_PRESET_ID || base.startsWith('wsl-')) continue
|
|
203
|
+
const target = `wsl-${base.toLowerCase()}`
|
|
204
|
+
if (!variants.has(target)) continue
|
|
205
|
+
if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue
|
|
206
|
+
inFlight.add(id)
|
|
207
|
+
void api.agentPresets.select({ sessionId: id, agentPreset: target })
|
|
208
|
+
.then((response: { result: { ok: boolean } }) => {
|
|
209
|
+
if (response.result.ok) sessions.noteAgentPreset(id, target)
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {
|
|
212
|
+
// A refused or aborted swap (session already produced output,
|
|
213
|
+
// roster churn, reconnect) leaves the session on its current
|
|
214
|
+
// composition; count the attempt so a stuck session stops
|
|
215
|
+
// retrying after MAX_ATTEMPTS.
|
|
216
|
+
attempts.set(id, (attempts.get(id) ?? 0) + 1)
|
|
217
|
+
})
|
|
218
|
+
.finally(() => {
|
|
219
|
+
inFlight.delete(id)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
maybeBind()
|
|
224
|
+
const unsubscribe = sessions.list.subscribe(() => maybeBind())
|
|
225
|
+
// Variants are generated at host boot; a page loaded before that would
|
|
226
|
+
// never see them without a periodic refresh.
|
|
227
|
+
const timer = window.setInterval(refreshRoster, 60_000)
|
|
228
|
+
return () => {
|
|
229
|
+
unsubscribe()
|
|
230
|
+
window.clearInterval(timer)
|
|
231
|
+
}
|
|
232
|
+
}, 'dsh-wsl-workspace: WSL mode-variant binding')
|
|
233
|
+
}
|