@goodandready/dsh-remote-workspace 0.3.6 → 0.3.8

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/lib/client.js CHANGED
@@ -117,6 +117,23 @@ window.__ModuleLoader__.load({
117
117
  title: 'Remote Workspace',
118
118
  subtitle: 'Native SSH connections, SFTP file access, 3-way mirror sync, and port forwarding tunnels.',
119
119
  status: 'Ready',
120
+ actionFailed: 'The request failed.',
121
+ versionUnknown: 'Version unknown',
122
+ updaterTitle: 'Plugin update',
123
+ currentVersion: 'Installed version',
124
+ latestVersion: 'Latest on npm',
125
+ checkUpdate: 'Check for updates',
126
+ checking: 'Checking...',
127
+ updateNow: 'Update now',
128
+ updating: 'Updating...',
129
+ upToDate: 'Up to date',
130
+ updateAvailable: 'Update available',
131
+ restartRequired: 'The update is installed. Restart DSH to use it.',
132
+ updateFailed: 'Update failed',
133
+ statusLoading: 'Checking connection',
134
+ statusEmpty: 'No active host',
135
+ statusReady: 'Host ready',
136
+ statusError: 'Status unavailable',
120
137
 
121
138
  tabProfiles: '🖥️ Host Profiles',
122
139
 
@@ -256,6 +273,12 @@ window.__ModuleLoader__.load({
256
273
  title: '远程开发工作区',
257
274
  subtitle: '支持原生 SSH 连接、SFTP 文件访问、3-way 镜像同步与端口转发隧道。',
258
275
  status: '就绪',
276
+ actionFailed: '请求失败。',
277
+ versionUnknown: '版本未知',
278
+ statusLoading: '正在检查连接',
279
+ statusEmpty: '没有活动主机',
280
+ statusReady: '主机已就绪',
281
+ statusError: '状态不可用',
259
282
 
260
283
  tabProfiles: '🖥️ 主机配置',
261
284
  tabContainers: '🐳 容器管理',
@@ -1135,237 +1158,31 @@ window.__ModuleLoader__.load({
1135
1158
  )
1136
1159
  }
1137
1160
 
1138
- function SettingsView(props) {
1139
- const [activeTab, setActiveTab] = React.useState('profiles')
1140
- const [healthData, setHealthData] = React.useState(null)
1141
- const [autoSync, setAutoSync] = React.useState(false)
1142
-
1143
- const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
1144
- const [profiles, setProfiles] = React.useState([])
1145
- const [activeId, setActiveId] = React.useState(null)
1146
- const [tunnels, setTunnels] = React.useState([])
1147
- const [editing, setEditing] = React.useState(null)
1148
- const [busy, setBusy] = React.useState(null)
1149
- const [testResult, setTestResult] = React.useState(null)
1150
- const [syncMsg, setSyncMsg] = React.useState(null)
1151
- const [tunnelMsg, setTunnelMsg] = React.useState(null)
1152
- const [showPassword, setShowPassword] = React.useState(false)
1153
-
1154
- // Tunnel creation inputs
1155
- const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
1156
- const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
1157
-
1158
- // Remote Directory Browser State
1159
- const [browserOpen, setBrowserOpen] = React.useState(false)
1160
- const [browserPath, setBrowserPath] = React.useState('/')
1161
- const [browserEntries, setBrowserEntries] = React.useState([])
1162
- const [browserLoading, setBrowserLoading] = React.useState(false)
1163
-
1164
- const loadState = async () => {
1165
- try {
1166
- const res = await fetch('/dsh-remote-workspace/state')
1167
- if (res.ok) {
1168
- const data = await res.json()
1169
- if (data.ok) {
1170
- setProfiles(data.profiles || [])
1171
- setActiveId(data.activeId || null)
1172
- setTunnels(data.tunnels || [])
1173
- setAutoSync(Boolean(data.autoSync))
1174
- if (data.activeId) {
1175
- fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
1176
- }
1177
- }
1178
- }
1179
- } catch (err) { /* best-effort cleanup */ }
1180
- }
1181
-
1182
- React.useEffect(() => {
1183
- ensureCss()
1184
- loadState()
1185
- }, [])
1186
-
1187
- const handleSave = async (profile) => {
1188
- setBusy('saving')
1189
- try {
1190
- const res = await fetch('/dsh-remote-workspace/profiles/save', {
1191
- method: 'POST',
1192
- headers: { 'Content-Type': 'application/json' },
1193
- body: JSON.stringify(profile)
1194
- })
1195
- if (res.ok) {
1196
- setEditing(null)
1197
- setBrowserOpen(false)
1198
- setTestResult(null)
1199
- await loadState()
1200
- }
1201
- } catch (err) { /* best-effort cleanup */ }
1202
- setBusy(null)
1203
- }
1204
-
1205
- const handleDelete = async (id, name) => {
1206
- if (!confirm(t('delConfirm', { name: name || id }))) return
1207
- try {
1208
- const res = await fetch('/dsh-remote-workspace/profiles/delete', {
1209
- method: 'POST',
1210
- headers: { 'Content-Type': 'application/json' },
1211
- body: JSON.stringify({ id })
1212
- })
1213
- if (res.ok) {
1214
- if (editing && editing.id === id) setEditing(null)
1215
- await loadState()
1216
- }
1217
- } catch (err) { /* best-effort cleanup */ }
1218
- }
1219
-
1220
- const handleSetActive = async (id) => {
1221
- try {
1222
- const res = await fetch('/dsh-remote-workspace/profiles/active', {
1223
- method: 'POST',
1224
- headers: { 'Content-Type': 'application/json' },
1225
- body: JSON.stringify({ id })
1226
- })
1227
- if (res.ok) {
1228
- await loadState()
1229
- }
1230
- } catch (err) { /* best-effort cleanup */ }
1231
- }
1232
-
1233
- const handleTest = async (prof) => {
1234
- setBusy('testing')
1235
- setTestResult(null)
1236
- try {
1237
- const res = await fetch('/dsh-remote-workspace/test', {
1238
- method: 'POST',
1239
- headers: { 'Content-Type': 'application/json' },
1240
- body: JSON.stringify({ profile: prof })
1241
- })
1242
- const data = await res.json()
1243
- setTestResult(data)
1244
- } catch (err) {
1245
- setTestResult({ ok: false, error: err.message })
1246
- }
1247
- setBusy(null)
1248
- }
1249
-
1250
- const openDirectoryBrowser = async (initialPath) => {
1251
- setBrowserOpen(true)
1252
- const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
1253
- await fetchRemoteDir(target)
1254
- }
1255
-
1256
- const fetchRemoteDir = async (dirPath) => {
1257
- setBrowserLoading(true)
1258
- try {
1259
- const res = await fetch('/dsh-remote-workspace/browse', {
1260
- method: 'POST',
1261
- headers: { 'Content-Type': 'application/json' },
1262
- body: JSON.stringify({ profile: editing, remotePath: dirPath })
1263
- })
1264
- const data = await res.json()
1265
- if (data.ok) {
1266
- setBrowserPath(data.currentPath || dirPath)
1267
- setBrowserEntries(data.entries || [])
1268
- }
1269
- } catch (err) { /* best-effort cleanup */ }
1270
- setBrowserLoading(false)
1271
- }
1272
-
1273
- const handleSync = async (direction) => {
1274
- setBusy(`sync_${direction}`)
1275
- setSyncMsg(null)
1276
- try {
1277
- const res = await fetch('/dsh-remote-workspace/sync', {
1278
- method: 'POST',
1279
- headers: { 'Content-Type': 'application/json' },
1280
- body: JSON.stringify({ direction })
1281
- })
1282
- const data = await res.json()
1283
- if (data.ok) {
1284
- const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
1285
- if (data.conflicts && data.conflicts.length > 0) {
1286
- setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
1287
- } else {
1288
- setSyncMsg({ ok: true, text: t('syncOk', { count }) })
1289
- }
1290
- } else {
1291
- setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
1292
- }
1293
- } catch (err) {
1294
- setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
1295
- }
1296
- setBusy(null)
1297
- }
1298
-
1299
- const handleStartTunnel = async () => {
1300
- if (!activeId) return
1301
- setBusy('tunnel_open')
1302
- setTunnelMsg(null)
1303
- try {
1304
- const res = await fetch('/dsh-remote-workspace/tunnels/start', {
1305
- method: 'POST',
1306
- headers: { 'Content-Type': 'application/json' },
1307
- body: JSON.stringify({
1308
- profileId: activeId,
1309
- localPort: newTunnelLocal,
1310
- remotePort: newTunnelRemote
1311
- })
1312
- })
1313
- const data = await res.json()
1314
- if (data.ok) {
1315
- setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
1316
- await loadState()
1317
- } else {
1318
- setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
1319
- }
1320
- } catch (err) {
1321
- setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
1322
- }
1323
- setBusy(null)
1324
- }
1325
-
1326
- const handleStopTunnel = async (tunnelId) => {
1327
- try {
1328
- const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
1329
- method: 'POST',
1330
- headers: { 'Content-Type': 'application/json' },
1331
- body: JSON.stringify({ tunnelId })
1332
- })
1333
- if (res.ok) {
1334
- await loadState()
1335
- }
1336
- } catch (err) { /* best-effort cleanup */ }
1337
- }
1338
-
1339
- const activeProfile = profiles.find((p) => p.id === activeId)
1340
-
1341
- return React.createElement(
1342
- 'div',
1343
- { className: 'drw-page' },
1344
-
1345
- // Header with Live Badges (ClineBot Style)
1346
- React.createElement(
1347
- 'div',
1348
- { className: 'drw-header' },
1349
- React.createElement(
1350
- 'div',
1351
- { className: 'drw-header-top' },
1352
- React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
1353
- React.createElement(
1354
- 'div',
1355
- { className: 'drw-header-badges' },
1356
- activeProfile
1357
- ? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
1358
- : React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
1359
- activeProfile
1360
- ? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
1361
- : null,
1362
- React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
1363
- )
1364
- ),
1365
- React.createElement('div', { className: 'drw-page-sub' }, t('subtitle'))
1366
- ),
1367
-
1368
- // Section 1: SSH Profiles Table & Management
1161
+ function ProfilesPanel(panel) {
1162
+ const {
1163
+ t,
1164
+ editing,
1165
+ setEditing,
1166
+ setTestResult,
1167
+ profiles,
1168
+ activeId,
1169
+ busy,
1170
+ handleSave,
1171
+ testResult,
1172
+ handleTest,
1173
+ handleSetActive,
1174
+ handleDelete,
1175
+ browserOpen,
1176
+ setBrowserOpen,
1177
+ browserPath,
1178
+ browserEntries,
1179
+ browserLoading,
1180
+ openDirectoryBrowser,
1181
+ fetchRemoteDir,
1182
+ showPassword,
1183
+ setShowPassword,
1184
+ } = panel
1185
+ return (
1369
1186
  React.createElement(
1370
1187
  'div',
1371
1188
  { className: 'drw-card' },
@@ -1826,104 +1643,67 @@ window.__ModuleLoader__.load({
1826
1643
  : !editing
1827
1644
  ? React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('noHosts'))
1828
1645
  : null
1829
- ),
1646
+ )
1647
+ )
1648
+ }
1830
1649
 
1831
- // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
1650
+ function SyncPanel(panel) {
1651
+ const {
1652
+ t,
1653
+ busy,
1654
+ activeProfile,
1655
+ handleSync,
1656
+ syncMsg,
1657
+ autoSync,
1658
+ setAutoSync,
1659
+ reportAction,
1660
+ setActionError,
1661
+ } = panel
1662
+ return (
1832
1663
  activeProfile
1833
1664
  ? React.createElement(
1834
1665
  'div',
1835
1666
  { className: 'drw-card' },
1667
+ React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
1668
+ React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
1669
+
1836
1670
  React.createElement(
1837
1671
  'div',
1838
- { className: 'drw-card-title' },
1839
- React.createElement('span', null, t('diagTitle')),
1672
+ { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
1673
+ React.createElement(
1674
+ 'div',
1675
+ { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
1676
+ React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
1677
+ React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
1678
+ ),
1840
1679
  React.createElement(
1841
1680
  'button',
1842
1681
  {
1843
1682
  type: 'button',
1844
- className: 'drw-btn',
1845
- style: { fontSize: '11px', padding: '3px 9px' },
1846
- disabled: busy === 'testing',
1847
- onClick: () => handleTest(activeProfile)
1683
+ className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
1684
+ style: { padding: '4px 12px', fontSize: '12px' },
1685
+ onClick: async () => {
1686
+ const next = !autoSync
1687
+ try {
1688
+ const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
1689
+ method: 'POST',
1690
+ headers: { 'Content-Type': 'application/json' },
1691
+ body: JSON.stringify({ enabled: next })
1692
+ })
1693
+ const data = await reportAction(res)
1694
+ if (data && data.ok) setAutoSync(data.autoSync)
1695
+ } catch (err) {
1696
+ setActionError(err && err.message ? err.message : t('actionFailed'))
1697
+ }
1698
+ }
1848
1699
  },
1849
- busy === 'testing' ? t('testing') : t('diagProbeBtn')
1700
+ autoSync ? '⚡ Active (ON)' : '⚪ Off'
1850
1701
  )
1851
1702
  ),
1852
- React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
1703
+
1853
1704
  React.createElement(
1854
1705
  'div',
1855
- { className: 'drw-grid-4' },
1856
- React.createElement(
1857
- 'div',
1858
- { className: 'drw-stat-box' },
1859
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
1860
- React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
1861
- ),
1862
- React.createElement(
1863
- 'div',
1864
- { className: 'drw-stat-box' },
1865
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
1866
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
1867
- ),
1868
- React.createElement(
1869
- 'div',
1870
- { className: 'drw-stat-box' },
1871
- React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
1872
- React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
1873
- ),
1874
- React.createElement(
1875
- 'div',
1876
- { className: 'drw-stat-box' },
1877
- React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
1878
- React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
1879
- )
1880
- )
1881
- )
1882
- : null,
1883
-
1884
- // Section 3: Mirror Sync (ClineBot Action Card style)
1885
- activeProfile
1886
- ? React.createElement(
1887
- 'div',
1888
- { className: 'drw-card' },
1889
- React.createElement('div', { className: 'drw-card-title' }, t('syncTitle')),
1890
- React.createElement('div', { className: 'drw-card-desc' }, t('syncDesc')),
1891
-
1892
- React.createElement(
1893
- 'div',
1894
- { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: '8px', border: '1px solid var(--dsw-alias-border-l2)', marginBottom: '12px' } },
1895
- React.createElement(
1896
- 'div',
1897
- { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
1898
- React.createElement('span', { style: { fontWeight: 600, fontSize: '13px' } }, t('autoSyncTitle')),
1899
- React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('autoSyncDesc'))
1900
- ),
1901
- React.createElement(
1902
- 'button',
1903
- {
1904
- type: 'button',
1905
- className: `drw-btn ${autoSync ? 'drw-btn-primary' : ''}`,
1906
- style: { padding: '4px 12px', fontSize: '12px' },
1907
- onClick: async () => {
1908
- const next = !autoSync
1909
- try {
1910
- const res = await fetch('/dsh-remote-workspace/watcher/toggle', {
1911
- method: 'POST',
1912
- headers: { 'Content-Type': 'application/json' },
1913
- body: JSON.stringify({ enabled: next })
1914
- })
1915
- const d = await res.json()
1916
- if (d.ok) setAutoSync(d.autoSync)
1917
- } catch (err) { /* best-effort cleanup */ }
1918
- }
1919
- },
1920
- autoSync ? '⚡ Active (ON)' : '⚪ Off'
1921
- )
1922
- ),
1923
-
1924
- React.createElement(
1925
- 'div',
1926
- { className: 'drw-row' },
1706
+ { className: 'drw-row' },
1927
1707
  React.createElement(
1928
1708
  'button',
1929
1709
  {
@@ -1958,9 +1738,25 @@ window.__ModuleLoader__.load({
1958
1738
  )
1959
1739
  : null
1960
1740
  )
1961
- : null,
1741
+ : null
1742
+ )
1743
+ }
1962
1744
 
1963
- // Section 4: SSH Port Forwarding (Tunnels Manager)
1745
+ function TunnelsPanel(panel) {
1746
+ const {
1747
+ t,
1748
+ busy,
1749
+ activeProfile,
1750
+ newTunnelLocal,
1751
+ setNewTunnelLocal,
1752
+ newTunnelRemote,
1753
+ setNewTunnelRemote,
1754
+ handleStartTunnel,
1755
+ tunnelMsg,
1756
+ tunnels,
1757
+ handleStopTunnel,
1758
+ } = panel
1759
+ return (
1964
1760
  React.createElement(
1965
1761
  'div',
1966
1762
  { className: 'drw-card' },
@@ -2077,6 +1873,330 @@ window.__ModuleLoader__.load({
2077
1873
  )
2078
1874
  }
2079
1875
 
1876
+ function SettingsView(props) {
1877
+ const [activeTab, setActiveTab] = React.useState('profiles')
1878
+ const [healthData, setHealthData] = React.useState(null)
1879
+ const [autoSync, setAutoSync] = React.useState(false)
1880
+
1881
+ const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
1882
+ const [profiles, setProfiles] = React.useState([])
1883
+ const [activeId, setActiveId] = React.useState(null)
1884
+ const [tunnels, setTunnels] = React.useState([])
1885
+ const [editing, setEditing] = React.useState(null)
1886
+ const [busy, setBusy] = React.useState(null)
1887
+ const [testResult, setTestResult] = React.useState(null)
1888
+ const [actionError, setActionError] = React.useState('')
1889
+ const [syncMsg, setSyncMsg] = React.useState(null)
1890
+ const [tunnelMsg, setTunnelMsg] = React.useState(null)
1891
+ const [showPassword, setShowPassword] = React.useState(false)
1892
+
1893
+ // Tunnel creation inputs
1894
+ const [newTunnelLocal, setNewTunnelLocal] = React.useState('3000')
1895
+ const [newTunnelRemote, setNewTunnelRemote] = React.useState('3000')
1896
+
1897
+ // Remote Directory Browser State
1898
+ const [browserOpen, setBrowserOpen] = React.useState(false)
1899
+ const [browserPath, setBrowserPath] = React.useState('/')
1900
+ const [browserEntries, setBrowserEntries] = React.useState([])
1901
+ const [browserLoading, setBrowserLoading] = React.useState(false)
1902
+
1903
+
1904
+ function actionErrorText(status, data) {
1905
+ if (status >= 200 && status < 300 && data && data.ok !== false) return ''
1906
+ if (data && typeof data.error === 'string' && data.error) return data.error
1907
+ return 'HTTP ' + status
1908
+ }
1909
+
1910
+ const reportAction = async (res) => {
1911
+ let data = null
1912
+ try { data = await res.json() } catch (err) { data = null }
1913
+ const text = actionErrorText(res.status, data)
1914
+ setActionError(text ? ((data && data.error) ? text : t('actionFailed')) : '')
1915
+ return text ? null : data
1916
+ }
1917
+
1918
+ const loadState = async () => {
1919
+ try {
1920
+ const res = await fetch('/dsh-remote-workspace/state')
1921
+ const data = await reportAction(res)
1922
+ if (data && data.ok) {
1923
+ setProfiles(data.profiles || [])
1924
+ setActiveId(data.activeId || null)
1925
+ setTunnels(data.tunnels || [])
1926
+ setAutoSync(Boolean(data.autoSync))
1927
+ if (data.activeId) {
1928
+ fetch(`/dsh-remote-workspace/health?profileId=${data.activeId}`).then(r => r.json()).then(h => { if (h.ok) setHealthData(h.health) }).catch(() => {})
1929
+ }
1930
+ }
1931
+ } catch (err) {
1932
+ setActionError(err && err.message ? err.message : t('actionFailed'))
1933
+ }
1934
+ }
1935
+
1936
+ React.useEffect(() => {
1937
+ ensureCss()
1938
+ loadState()
1939
+ }, [])
1940
+
1941
+ const handleSave = async (profile) => {
1942
+ setBusy('saving')
1943
+ try {
1944
+ const res = await fetch('/dsh-remote-workspace/profiles/save', {
1945
+ method: 'POST',
1946
+ headers: { 'Content-Type': 'application/json' },
1947
+ body: JSON.stringify(profile)
1948
+ })
1949
+ const data = await reportAction(res)
1950
+ if (data) {
1951
+ setEditing(null)
1952
+ setBrowserOpen(false)
1953
+ setTestResult(null)
1954
+ await loadState()
1955
+ }
1956
+ } catch (err) {
1957
+ setActionError(err && err.message ? err.message : t('actionFailed'))
1958
+ }
1959
+ setBusy(null)
1960
+ }
1961
+
1962
+ const handleDelete = async (id, name) => {
1963
+ if (!confirm(t('delConfirm', { name: name || id }))) return
1964
+ try {
1965
+ const res = await fetch('/dsh-remote-workspace/profiles/delete', {
1966
+ method: 'POST',
1967
+ headers: { 'Content-Type': 'application/json' },
1968
+ body: JSON.stringify({ id })
1969
+ })
1970
+ const data = await reportAction(res)
1971
+ if (data) {
1972
+ if (editing && editing.id === id) setEditing(null)
1973
+ await loadState()
1974
+ }
1975
+ } catch (err) {
1976
+ setActionError(err && err.message ? err.message : t('actionFailed'))
1977
+ }
1978
+ }
1979
+
1980
+ const handleSetActive = async (id) => {
1981
+ try {
1982
+ const res = await fetch('/dsh-remote-workspace/profiles/active', {
1983
+ method: 'POST',
1984
+ headers: { 'Content-Type': 'application/json' },
1985
+ body: JSON.stringify({ id })
1986
+ })
1987
+ const data = await reportAction(res)
1988
+ if (data) await loadState()
1989
+ } catch (err) {
1990
+ setActionError(err && err.message ? err.message : t('actionFailed'))
1991
+ }
1992
+ }
1993
+
1994
+ const handleTest = async (prof) => {
1995
+ setBusy('testing')
1996
+ setTestResult(null)
1997
+ try {
1998
+ const res = await fetch('/dsh-remote-workspace/test', {
1999
+ method: 'POST',
2000
+ headers: { 'Content-Type': 'application/json' },
2001
+ body: JSON.stringify(prof)
2002
+ })
2003
+ const data = await res.json()
2004
+ setTestResult(data)
2005
+ } catch (err) {
2006
+ setTestResult({ ok: false, error: err.message })
2007
+ }
2008
+ setBusy(null)
2009
+ }
2010
+
2011
+ const openDirectoryBrowser = async (initialPath) => {
2012
+ setBrowserOpen(true)
2013
+ const target = initialPath && initialPath.startsWith('/') ? initialPath : '/'
2014
+ await fetchRemoteDir(target)
2015
+ }
2016
+
2017
+ const fetchRemoteDir = async (dirPath) => {
2018
+ setBrowserLoading(true)
2019
+ try {
2020
+ const res = await fetch('/dsh-remote-workspace/browse', {
2021
+ method: 'POST',
2022
+ headers: { 'Content-Type': 'application/json' },
2023
+ body: JSON.stringify({ profile: editing, remotePath: dirPath })
2024
+ })
2025
+ const data = await reportAction(res)
2026
+ if (data && data.ok) {
2027
+ setBrowserPath(data.currentPath || dirPath)
2028
+ setBrowserEntries(data.entries || [])
2029
+ }
2030
+ } catch (err) {
2031
+ setActionError(err && err.message ? err.message : t('actionFailed'))
2032
+ }
2033
+ setBrowserLoading(false)
2034
+ }
2035
+
2036
+ const handleSync = async (direction) => {
2037
+ setBusy(`sync_${direction}`)
2038
+ setSyncMsg(null)
2039
+ try {
2040
+ const res = await fetch('/dsh-remote-workspace/sync', {
2041
+ method: 'POST',
2042
+ headers: { 'Content-Type': 'application/json' },
2043
+ body: JSON.stringify({ direction })
2044
+ })
2045
+ const data = await res.json()
2046
+ if (data.ok) {
2047
+ const count = (direction === 'push' ? data.pushed?.length : data.pulled?.length) || 0
2048
+ if (data.conflicts && data.conflicts.length > 0) {
2049
+ setSyncMsg({ ok: false, text: t('syncConflict', { count: data.conflicts.length }) })
2050
+ } else {
2051
+ setSyncMsg({ ok: true, text: t('syncOk', { count }) })
2052
+ }
2053
+ } else {
2054
+ setSyncMsg({ ok: false, text: t('syncErr', { err: data.error || 'Unknown error' }) })
2055
+ }
2056
+ } catch (err) {
2057
+ setSyncMsg({ ok: false, text: t('syncErr', { err: err.message }) })
2058
+ }
2059
+ setBusy(null)
2060
+ }
2061
+
2062
+ const handleStartTunnel = async () => {
2063
+ if (!activeId) return
2064
+ setBusy('tunnel_open')
2065
+ setTunnelMsg(null)
2066
+ try {
2067
+ const res = await fetch('/dsh-remote-workspace/tunnels/start', {
2068
+ method: 'POST',
2069
+ headers: { 'Content-Type': 'application/json' },
2070
+ body: JSON.stringify({
2071
+ profileId: activeId,
2072
+ localPort: newTunnelLocal,
2073
+ remotePort: newTunnelRemote
2074
+ })
2075
+ })
2076
+ const data = await res.json()
2077
+ if (data.ok) {
2078
+ setTunnelMsg({ ok: true, text: t('tOpenOk', { localPort: newTunnelLocal, remotePort: newTunnelRemote }) })
2079
+ await loadState()
2080
+ } else {
2081
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: data.error || 'Failed' }) })
2082
+ }
2083
+ } catch (err) {
2084
+ setTunnelMsg({ ok: false, text: t('tOpenErr', { err: err.message }) })
2085
+ }
2086
+ setBusy(null)
2087
+ }
2088
+
2089
+ const handleStopTunnel = async (tunnelId) => {
2090
+ try {
2091
+ const res = await fetch('/dsh-remote-workspace/tunnels/stop', {
2092
+ method: 'POST',
2093
+ headers: { 'Content-Type': 'application/json' },
2094
+ body: JSON.stringify({ tunnelId })
2095
+ })
2096
+ const data = await reportAction(res)
2097
+ if (data) await loadState()
2098
+ } catch (err) {
2099
+ setActionError(err && err.message ? err.message : t('actionFailed'))
2100
+ }
2101
+ }
2102
+
2103
+ const activeProfile = profiles.find((p) => p.id === activeId)
2104
+
2105
+ return React.createElement(
2106
+ 'div',
2107
+ { className: 'drw-page' },
2108
+
2109
+ // Header with Live Badges (ClineBot Style)
2110
+ React.createElement(
2111
+ 'div',
2112
+ { className: 'drw-header' },
2113
+ React.createElement(
2114
+ 'div',
2115
+ { className: 'drw-header-top' },
2116
+ React.createElement('div', { className: 'drw-page-title' }, '🌐 ' + t('title')),
2117
+ React.createElement(
2118
+ 'div',
2119
+ { className: 'drw-header-badges' },
2120
+ activeProfile
2121
+ ? React.createElement('span', { className: 'drw-badge drw-badge-ok' }, t('badgeOnline', { name: activeProfile.name || activeProfile.host, latency: testResult?.latencyMs ?? 15 }))
2122
+ : React.createElement('span', { className: 'drw-badge drw-badge-warn' }, t('badgeOffline')),
2123
+ activeProfile
2124
+ ? React.createElement('span', { className: 'drw-badge' }, activeProfile.authType === 'password' ? t('badgePassword') : t('badgeKey'))
2125
+ : null,
2126
+ React.createElement('span', { className: `drw-badge ${tunnels.length > 0 ? 'drw-badge-ok' : ''}` }, t('badgeTunnels', { count: tunnels.length }))
2127
+ )
2128
+ ),
2129
+ React.createElement('div', { className: 'drw-page-sub' }, t('subtitle')),
2130
+ actionError
2131
+ ? React.createElement('div', { className: 'drw-badge drw-badge-err', role: 'alert' }, actionError)
2132
+ : null
2133
+ ),
2134
+
2135
+
2136
+ React.createElement(ProfilesPanel, { t, editing, setEditing, setTestResult, profiles, activeId, busy, handleSave, testResult, handleTest, handleSetActive, handleDelete, browserOpen, setBrowserOpen, browserPath, browserEntries, browserLoading, openDirectoryBrowser, fetchRemoteDir, showPassword, setShowPassword }),
2137
+
2138
+
2139
+ // Section 2: Active Connection Diagnostics (ClineBot Card 4 style)
2140
+ activeProfile
2141
+ ? React.createElement(
2142
+ 'div',
2143
+ { className: 'drw-card' },
2144
+ React.createElement(
2145
+ 'div',
2146
+ { className: 'drw-card-title' },
2147
+ React.createElement('span', null, t('diagTitle')),
2148
+ React.createElement(
2149
+ 'button',
2150
+ {
2151
+ type: 'button',
2152
+ className: 'drw-btn',
2153
+ style: { fontSize: '11px', padding: '3px 9px' },
2154
+ disabled: busy === 'testing',
2155
+ onClick: () => handleTest(activeProfile)
2156
+ },
2157
+ busy === 'testing' ? t('testing') : t('diagProbeBtn')
2158
+ )
2159
+ ),
2160
+ React.createElement('div', { className: 'drw-card-desc' }, t('diagDesc')),
2161
+ React.createElement(
2162
+ 'div',
2163
+ { className: 'drw-grid-4' },
2164
+ React.createElement(
2165
+ 'div',
2166
+ { className: 'drw-stat-box' },
2167
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.name || activeProfile.host),
2168
+ React.createElement('div', { className: 'drw-stat-lbl' }, `SSH: ${activeProfile.username}@${activeProfile.host}:${activeProfile.port || 22}`)
2169
+ ),
2170
+ React.createElement(
2171
+ 'div',
2172
+ { className: 'drw-stat-box' },
2173
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.remoteWorkspace || '—'),
2174
+ React.createElement('div', { className: 'drw-stat-lbl' }, 'Remote Workspace')
2175
+ ),
2176
+ React.createElement(
2177
+ 'div',
2178
+ { className: 'drw-stat-box' },
2179
+ React.createElement('div', { className: 'drw-stat-val' }, activeProfile.localMirrorPath || '—'),
2180
+ React.createElement('div', { className: 'drw-stat-lbl' }, 'Local Mirror Path')
2181
+ ),
2182
+ React.createElement(
2183
+ 'div',
2184
+ { className: 'drw-stat-box' },
2185
+ React.createElement('div', { className: 'drw-stat-val', style: { color: 'var(--dsw-alias-state-success-primary)' } }, 'Ready ✓'),
2186
+ React.createElement('div', { className: 'drw-stat-lbl' }, activeProfile.authType === 'password' ? 'Password Auth' : 'SSH Key Auth')
2187
+ )
2188
+ )
2189
+ )
2190
+ : null,
2191
+
2192
+
2193
+ React.createElement(SyncPanel, { t, busy, activeProfile, handleSync, syncMsg, autoSync, setAutoSync, reportAction, setActionError }),
2194
+
2195
+ React.createElement(TunnelsPanel, { t, busy, activeProfile, newTunnelLocal, setNewTunnelLocal, newTunnelRemote, setNewTunnelRemote, handleStartTunnel, tunnelMsg, tunnels, handleStopTunnel }),
2196
+
2197
+ )
2198
+ }
2199
+
2080
2200
  // Accordion item for Settings -> Plugins section
2081
2201
 
2082
2202
  function UpdaterSection(props) {
@@ -2154,8 +2274,8 @@ window.__ModuleLoader__.load({
2154
2274
  React.createElement(
2155
2275
  'div',
2156
2276
  null,
2157
- React.createElement('span', { style: { fontWeight: 500 } }, 'v' + (updateStatus?.currentVersion || '0.3.1')),
2158
- updateStatus?.latestVersion && updateStatus.latestVersion !== updateStatus.currentVersion
2277
+ React.createElement('span', { style: { fontWeight: 500 } }, (updateStatus?.currentVersion ? 'v' + updateStatus.currentVersion : t('versionUnknown'))),
2278
+ updateStatus?.updateAvailable
2159
2279
  ? React.createElement('span', { style: { marginLeft: '8px', color: 'var(--dsw-alias-state-warning-text)' } }, '→ v' + updateStatus.latestVersion)
2160
2280
  : null,
2161
2281
  msg
@@ -2198,10 +2318,34 @@ window.__ModuleLoader__.load({
2198
2318
  const [open, setOpen] = React.useState(!!page)
2199
2319
  const t = props.t || (props.locale === 'zh' ? makeT(zh, en) : makeT(en, zh))
2200
2320
 
2321
+ const [conn, setConn] = React.useState('loading')
2322
+
2201
2323
  React.useEffect(() => {
2202
2324
  ensureCss()
2203
2325
  }, [])
2204
2326
 
2327
+ React.useEffect(() => {
2328
+ let cancelled = false
2329
+ fetch('/dsh-remote-workspace/state')
2330
+ .then((res) => {
2331
+ if (!res.ok) throw new Error('HTTP ' + res.status)
2332
+ return res.json()
2333
+ })
2334
+ .then((data) => {
2335
+ if (cancelled) return
2336
+ if (!data || data.ok === false) {
2337
+ setConn('error')
2338
+ return
2339
+ }
2340
+ const active = (data.profiles || []).find((item) => item.id === data.activeId)
2341
+ setConn(active ? 'ready' : 'empty')
2342
+ })
2343
+ .catch(() => {
2344
+ if (!cancelled) setConn('error')
2345
+ })
2346
+ return () => { cancelled = true }
2347
+ }, [])
2348
+
2205
2349
  // Row seat (plugins.row.config): the host page draws title/icon/crumb and the
2206
2350
  // padding, so the summary is a one-liner and the page drops our card chrome.
2207
2351
  if (props && props.view === 'summary') {
@@ -2235,11 +2379,22 @@ window.__ModuleLoader__.load({
2235
2379
  React.createElement('div', { style: { fontWeight: 600, fontSize: '15px', color: 'var(--dsw-alias-label-primary)' } }, t('title')),
2236
2380
  React.createElement('div', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('subtitle'))
2237
2381
  ),
2238
- React.createElement('span', { className: 'drw-badge drw-badge-ok', style: { marginRight: '14px' } }, t('status')),
2382
+ React.createElement('span', {
2383
+ className: 'drw-badge ' + (
2384
+ conn === 'ready' ? 'drw-badge-ok'
2385
+ : conn === 'error' ? 'drw-badge-err'
2386
+ : conn === 'empty' ? 'drw-badge-warn' : ''
2387
+ ),
2388
+ style: { marginRight: '14px' }
2389
+ }, t(
2390
+ conn === 'ready' ? 'statusReady'
2391
+ : conn === 'error' ? 'statusError'
2392
+ : conn === 'empty' ? 'statusEmpty' : 'statusLoading'
2393
+ )),
2239
2394
  React.createElement(
2240
2395
  'span',
2241
2396
  { style: { transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .16s' } },
2242
- React.createElement(FallbackChevron)
2397
+ React.createElement(ChevronIcon || FallbackChevron)
2243
2398
  )
2244
2399
  ),
2245
2400
  (page || open)
@@ -2301,16 +2456,28 @@ window.__ModuleLoader__.load({
2301
2456
 
2302
2457
  if (ctx.slots && typeof ctx.slots.inject === 'function') {
2303
2458
  // Plugin-list seat (plugins.item) first: the seat the current core
2304
- // (0.1.6-alpha.2) renders as the plugin's own page with its configuration. The
2305
- // label is a static string on purpose — it is resolved while the page renders,
2306
- // and a locale lookup there would take the whole client batch down with it.
2459
+ // (0.1.6-alpha.2) renders as the plugin's own page with its configuration.
2460
+ // The label getter runs during page render. A locale-service lookup there
2461
+ // crashes the client batch, so the title is read from the dictionaries
2462
+ // already loaded in this bundle. English is the result when the active
2463
+ // language cannot be read.
2307
2464
  ctx.slots.inject('plugins.item', () =>
2308
2465
  ctx.slots.register(
2309
2466
  {
2310
2467
  name: 'plugins.item',
2311
2468
  id: ROW_ID,
2312
2469
  order: 60,
2313
- label: () => 'Remote Workspace',
2470
+ label: () => {
2471
+ try {
2472
+ const lang = ctx.locale && typeof ctx.locale.getLocale === 'function'
2473
+ ? ctx.locale.getLocale()
2474
+ : ''
2475
+ if (typeof lang === 'string' && (lang === 'zh' || lang.indexOf('zh') === 0)) return zh.title
2476
+ } catch (err) {
2477
+ return en.title
2478
+ }
2479
+ return en.title
2480
+ },
2314
2481
  locale: NS,
2315
2482
  inject: () => ({ ctx })
2316
2483
  },