/** * Browser half: a "工作区隔离 / Workspace isolation" page inside Settings → * Plugins. It lists every Workspace with the isolation switch and toggles one * workspace at a time through the host half's routes. * * The module is a lazy CommonJS factory registered on the client module table — * the same shape the packaged client bundles use. It requires only `react` and * the framework's slot service; every other dependency would need its own * factory arrival, which a hand-written bundle cannot declare. */ window.__ModuleLoader__.load({ id: 'dsh-session-isolation', factory: (require) => { const module = { exports: {} } const exports = module.exports Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }) const React = require('react') const { useEffect, useState } = React /** Host routes owned by the Node half. */ const LIST_ROUTE = '/session-isolation/workspaces' const TOGGLE_ROUTE = '/session-isolation/toggle' /** One page's copy, kept in one place so a later locale pass has a single seam. */ const TEXT = { tab: '工作区隔离', title: '工作区隔离', intro: '开启后,该工作区里每个新会话都在自己的 session- 子目录中工作,' + '该子目录同时是它的文件沙箱写入边界,因此并行会话不会互相覆盖文件。已存在的会话不受影响。', empty: '还没有任何工作区。先在侧边栏打开一个项目目录。', loading: '读取中…', retry: '重试', on: '已开启', off: '未开启', enable: '开启隔离', disable: '关闭隔离', sessions: '会话', isolatedCount: (count) => `已开启隔离的工作区:${count}`, failed: (message) => `操作失败:${message}`, unavailable: '宿主没有响应这个插件的接口,可能插件未加载或宿主未挂载 Web 服务。', } /** Read the workspace list plus isolation state from the host half. */ async function fetchState() { const response = await fetch(LIST_ROUTE, { headers: { accept: 'application/json' } }) if (!response.ok) throw new Error(`${response.status} ${response.statusText}`) return await response.json() } /** Toggle one workspace and return the refreshed snapshot. */ async function toggle(workspaceId, enabled) { const response = await fetch(TOGGLE_ROUTE, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify({ workspaceId, enabled }), }) const payload = await response.json().catch(() => ({})) if (!response.ok) throw new Error(payload?.error ?? `${response.status} ${response.statusText}`) return payload } const styles = { root: { padding: '4px 0 24px', color: 'var(--dsw-alias-label-primary)', fontSize: 13, lineHeight: 1.6 }, title: { margin: '0 0 6px', fontSize: 14, fontWeight: 600 }, intro: { margin: '0 0 16px', color: 'var(--dsw-alias-label-secondary)', fontSize: 12 }, summary: { margin: '0 0 12px', color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 }, row: { display: 'flex', alignItems: 'center', gap: 12, padding: '10px 0', borderTop: '0.5px solid var(--dsw-alias-border-l2)', }, main: { minWidth: 0, flex: 1 }, name: { fontWeight: 500 }, path: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, state: { flex: 'none', color: 'var(--dsw-alias-label-secondary)', fontSize: 12, minWidth: 48, textAlign: 'right' }, button: { flex: 'none', font: 'inherit', fontSize: 12, cursor: 'pointer', color: 'var(--dsw-alias-label-primary)', background: 'var(--dsw-alias-bg-layer-3)', border: '0.5px solid var(--dsw-alias-border-l4)', borderRadius: 8, padding: '5px 12px', }, buttonBusy: { opacity: 0.6, cursor: 'default' }, error: { color: 'var(--dsw-alias-state-error-primary)', fontSize: 12, margin: '8px 0 0' }, empty: { color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 }, } /** * The isolation page. * @returns the rendered page. */ function SessionIsolationPage() { const [state, setState] = useState(undefined) const [error, setError] = useState(undefined) const [busyId, setBusyId] = useState(undefined) const load = () => { fetchState().then( (next) => { setState(next); setError(undefined) }, (cause) => { setError(cause?.message ?? String(cause)) }, ) } useEffect(() => { load() // Another surface (conversation tool, marker file) may change a decision // while this page is open, so the list refreshes on an interval. const timer = setInterval(load, 5000) return () => { clearInterval(timer) } }, []) const apply = async (workspaceId, enabled) => { setBusyId(workspaceId) try { setState(await toggle(workspaceId, enabled)) setError(undefined) } catch (cause) { setError(cause?.message ?? String(cause)) load() } finally { setBusyId(undefined) } } const workspaces = state?.workspaces ?? [] return React.createElement( 'div', { style: styles.root }, React.createElement('h3', { style: styles.title }, TEXT.title), React.createElement('p', { style: styles.intro }, TEXT.intro), state === undefined && error === undefined ? React.createElement('p', { style: styles.empty }, TEXT.loading) : null, state !== undefined && workspaces.length > 0 ? React.createElement('p', { style: styles.summary }, TEXT.isolatedCount(state.isolatedCount ?? 0)) : null, state !== undefined && workspaces.length === 0 ? React.createElement('p', { style: styles.empty }, TEXT.empty) : null, workspaces.map((workspace) => React.createElement( 'div', { key: workspace.workspaceId, style: styles.row }, React.createElement( 'div', { style: styles.main }, React.createElement('div', { style: styles.name }, workspace.title), React.createElement('div', { style: styles.path, title: workspace.path }, workspace.path), ), React.createElement( 'span', { style: styles.state }, `${TEXT.sessions} ${workspace.sessionCount} · ${workspace.isolates ? TEXT.on : TEXT.off}`, ), React.createElement( 'button', { type: 'button', style: busyId === workspace.workspaceId ? { ...styles.button, ...styles.buttonBusy } : styles.button, disabled: busyId === workspace.workspaceId, onClick: () => { void apply(workspace.workspaceId, !workspace.isolates) }, }, workspace.isolates ? TEXT.disable : TEXT.enable, ), )), error === undefined ? null : React.createElement( 'p', { style: styles.error }, error === 'Failed to fetch' || error.includes('404') ? TEXT.unavailable : TEXT.failed(error), ' ', React.createElement('button', { type: 'button', style: { ...styles.button, marginLeft: 8 }, onClick: load, }, TEXT.retry), ), ) } /** Client plugin dependencies: the slot ledger only. */ const inject = ['slots'] /** * Contribute the isolation page to Settings → Plugins. * @param ctx - the client context. */ function apply(ctx) { // The Settings owner declares this slot when its bundle activates. Wait // for the declaration instead of registering eagerly: plugin activation // order is not stable, and registering into an undeclared slot fails the // whole browser boot. ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({ name: 'settings.plugins.tab', id: 'session-isolation', order: 90, label: TEXT.tab, }, SessionIsolationPage)) } exports.apply = apply exports.inject = inject return module.exports }, })