@goodandready/dsh-remote-workspace 0.3.7 → 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/README.md CHANGED
@@ -127,8 +127,10 @@ Four orthogonal, high-leverage tools exposed directly to LLM agents:
127
127
  - Designed strictly to DSH UX guidelines and styled after `dsh-clinebot`.
128
128
  - **Segmented Auth Switcher**: Clean tabbed toggle between Private Key and Password authentication.
129
129
  - **Remote Directory Browser Modal**: Interactive remote file browser with breadcrumb navigation and one-click path selection.
130
- - **Connection Diagnostic Badge**: Real-time ping testing with visual latency indicators and remote OS display.
131
- - **Action Triggers**: Quick buttons for directional synchronization and tunnel monitoring.
130
+ - **Connection Diagnostic Badge**: Real-time ping testing with visual latency indicators and remote OS display. The header badge follows `/dsh-remote-workspace/state`: it shows a loading, empty, ready, or unavailable connection instead of a permanent Ready label.
131
+ - **Action Triggers**: Quick buttons for directional synchronization and tunnel monitoring. A failed save, delete, activation, directory browse, or tunnel stop shows the server error in an alert on the card. Test Connection posts the profile fields, including `host`.
132
+ - **Plugin list label**: English `Remote Workspace` or Chinese `远程开发工作区`, taken from the dictionaries already loaded with the card.
133
+ - **Updater version**: the row shows the installed version returned by the status request. Before that response it shows "Version unknown".
132
134
 
133
135
  ---
134
136
 
package/README.ru.md CHANGED
@@ -127,8 +127,10 @@ graph LR
127
127
  - Полноценная интеграция в слот `settings.plugin.item` с ключом `dsh-remote-workspace`.
128
128
  - **Сегментированный переключатель авторизации**: Удобное переключение между SSH-ключом и паролем.
129
129
  - **Интерактивный браузер удалённых папок**: Модальное окно с навигацией по каталогам и выбором рабочей директории в один клик.
130
- - **Индикатор статуса и задержки**: Проверка связи с отображением пинга и ОС удалённого узла.
131
- - **Кнопки действий**: Быстрый запуск синхронизации и контроль туннелей прямо из настроек.
130
+ - **Индикатор статуса и задержки**: Проверка связи с отображением пинга и ОС удалённого узла. Бейдж шапки читает `/dsh-remote-workspace/state` и показывает проверку, отсутствие активного хоста, готовность или недоступность статуса.
131
+ - **Кнопки действий**: Быстрый запуск синхронизации и контроль туннелей прямо из настроек. Ошибка сохранения, удаления, выбора активного профиля, обзора каталога или остановки туннеля показывается в предупреждении на карточке. Проверка соединения отправляет поля профиля, включая `host`.
132
+ - **Подпись в списке плагинов**: английская `Remote Workspace` или китайская `远程开发工作区` из словарей карточки.
133
+ - **Версия в блоке обновления**: строка показывает установленную версию из ответа статуса. Пока ответ не пришёл, показывается нейтральная подпись неизвестной версии.
132
134
 
133
135
  ---
134
136
 
package/README.zh.md CHANGED
@@ -127,8 +127,10 @@ graph LR
127
127
  - 深度适配 `settings.plugin.item` 插槽(Key: `dsh-remote-workspace`)。
128
128
  - **分段式认证切换器**:优雅切换私钥认证与密码认证。
129
129
  - **远程目录浏览器弹窗**:可视化浏览远程服务器目录树,支持面包屑导航与一键选取。
130
- - **实时连接状态徽章**:可视化展示连通性、网络延迟以及远程操作系统信息。
131
- - **快捷动作触发**:一键发起定向文件同步并监控隧道运行状态。
130
+ - **实时连接状态徽章**:可视化展示连通性、网络延迟以及远程操作系统信息。页眉徽章读取 `/dsh-remote-workspace/state`,分别表示正在检查、没有活动主机、主机已就绪或状态不可用,不再固定显示“就绪”。
131
+ - **快捷动作触发**:一键发起定向文件同步并监控隧道运行状态。保存、删除、设为活动、浏览目录或关闭隧道失败时,卡片上的警告会显示服务器返回的错误。连接测试提交的是配置本身,其中包含 `host`。
132
+ - **插件列表名称**:英文为 `Remote Workspace`,中文为 `远程开发工作区`,取自随卡片加载的词典。
133
+ - **更新版本**:该行显示状态接口返回的已安装版本。结果返回前显示“版本未知”。
132
134
 
133
135
  ---
134
136
 
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
  },
@@ -31,21 +31,9 @@ function isLoopback(value) {
31
31
  )
32
32
  }
33
33
 
34
- function isPrivateLan(value) {
35
- const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
36
- if (!address) return false
37
- const ipv4 = address.startsWith('::ffff:') ? address.slice(7) : address
38
- return (
39
- ipv4.startsWith('192.168.') ||
40
- ipv4.startsWith('10.') ||
41
- /^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(ipv4)
42
- )
43
- }
44
-
45
34
  export function isTrustedUpdateRequest(request) {
46
35
  if (header(request, UPDATE_HEADER) !== '1') return false
47
- const remote = request.socket?.remoteAddress
48
- if (!isLoopback(remote) && !isPrivateLan(remote)) return false
36
+ if (!isLoopback(request.socket?.remoteAddress)) return false
49
37
  const site = header(request, 'sec-fetch-site')
50
38
  if (site !== undefined && site !== 'same-origin') return false
51
39
  const origin = header(request, 'origin')
@@ -53,11 +41,8 @@ export function isTrustedUpdateRequest(request) {
53
41
  if (origin === undefined || host === undefined) return false
54
42
  try {
55
43
  const url = new URL(origin)
56
- return (
57
- (url.protocol === 'http:' || url.protocol === 'https:') &&
58
- (isLoopback(url.hostname) || isPrivateLan(url.hostname)) &&
59
- url.host === host
60
- )
44
+ return (url.protocol === 'http:' || url.protocol === 'https:')
45
+ && isLoopback(url.hostname) && url.host === host
61
46
  } catch (err) { /* best-effort fallback */
62
47
  return false
63
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-remote-workspace",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Enterprise Remote Workspace Plugin for DeepSeek Harness: SSH2 connection pool, SFTP operations, 3-way conflict-aware mirror sync, port forwarding, and native DSH UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -16,7 +16,6 @@
16
16
  "README.md",
17
17
  "README.ru.md",
18
18
  "README.zh.md",
19
- "docs/design/DESIGN.md",
20
19
  "LICENSE"
21
20
  ],
22
21
  "scripts": {
@@ -1,91 +0,0 @@
1
- # DESIGN.md — dsh-remote-workspace
2
-
3
- ## Product / Purpose
4
- - **Назначение**: Полнофункциональный корпоративный плагин удалённой разработки для DeepSeek Harness (DSH). Обеспечивает подключение к удалённым серверам по SSH (ключ / пароль), SFTP-навигацию, 3-way синхронизацию локального зеркала, SSH-туннелирование (port forwarding) и компактный набор инструментов для LLM агента (
5
- emote_exec,
6
- emote_fs,
7
- emote_sync,
8
- emote_tunnel).
9
- - **Аудитория**: Разработчики и инженеры, использующие DSH для работы с проектами на VPS, удаленных серверах, облачных инстансах и MiniPC.
10
- - **Статус**: Стабильный релиз v0.3.3 (One-Click Updater, Cross-Plugin API, Clean Tokens, Modular Routes).
11
-
12
- ## User Surfaces
13
- - **Web/UI**: Нативная карточка настроек профилей хостов (settings.plugin.item), удаленный SFTP браузер каталогов, чип статуса подключения в шапке сессии (conversation.session.header.utilities).
14
- - **DSH UI / settings / slots**:
15
- - Карточка плагина: settings.plugin.item для @goodandready/dsh-remote-workspace.
16
- - Статусный чип сессии: conversation.session.header.utilities (order: 30).
17
- - Запрещённые слоты: settings.section (запрещён стандартом authoring, все настройки только внутри settings.plugin.item).
18
- - **API маршруты**:
19
- - GET /dsh-remote-workspace/state: получение профилей, активного хоста и туннелей.
20
- - POST /dsh-remote-workspace/profiles/save: сохранение профиля (с валидацией и сбросом старых соединений).
21
- - POST /dsh-remote-workspace/profiles/delete: удаление профиля.
22
- - POST /dsh-remote-workspace/profiles/active: переключение активного профиля.
23
- - POST /dsh-remote-workspace/test: live-проверка связи (Ping, OS, Latency).
24
- - POST /dsh-remote-workspace/browse: чтение содержимого удалённой папки (SFTP).
25
- - POST /dsh-remote-workspace/sync: запуск двусторонней 3-way синхронизации зеркала (pull/push).
26
- - POST /dsh-remote-workspace/tunnels/start: создание локального SSH-туннеля.
27
- - POST /dsh-remote-workspace/tunnels/stop: остановка активного SSH-туннеля.
28
- - **CLI / Model Tools**:
29
- -
30
- emote_exec: выполнение команд на сервере.
31
- -
32
- emote_fs: чтение, атомарная запись, stat, mkdir, remove.
33
- -
34
- emote_sync: запуск 3-way синхронизации с контролем конфликтов.
35
- -
36
- emote_tunnel: создание/остановка/листинг туннелей.
37
-
38
- ## Visual Direction (Единый стиль dsh-clinebot)
39
- - **Атмосфера**: Премиальный инженерный интерфейс уровня VS Code Remote / Linear. Строгая типографика, плотные функциональные карточки, чистые переходы и отсутствие лишних теней.
40
- - **Токены ядра DSH**:
41
- - Фоны: ar(--dsw-alias-bg-layer-3) (карточки), ar(--dsw-alias-bg-layer-2) (поля ввода, вложенные блоки, таблицы), ar(--dsw-alias-bg-layer-1) (сегменты, подложка).
42
- - Границы: ar(--dsw-alias-border-l2).
43
- - Текст: ar(--dsw-alias-label-primary), ar(--dsw-alias-label-secondary), ar(--dsw-alias-label-dimmed).
44
- - Статусы: ar(--dsw-alias-state-success-primary), ar(--dsw-alias-state-warning-primary), ar(--dsw-alias-state-error-primary), ar(--dsw-alias-state-brand-primary).
45
- - **Шапка карточки**:
46
- - Название плагина, подзаголовок, статусный аккордеонный шеврон.
47
- - Статусные пилюли (Badges): Host online / Offline, Active Profile, Active Tunnels count.
48
-
49
- ## Components And States
50
- 1. **Секция 1: Профили серверов (SSH Profiles)**:
51
- - Таблица настроенных машин с бейджами типа аутентификации (🔑 key / 🔒 pwd), меткой активного сервера, кнопками «Сделать активным», «Редактировать», «Удалить».
52
- - Пустое состояние: ясное сообщение и кнопка + Добавить хост.
53
- 2. **Секция 2: Интерактивный редактор профиля**:
54
- - Название, Хост/IP, Порт, Пользователь.
55
- - Сегментированный переключатель авторизации (SSH-ключ vs Пароль).
56
- - Поля авторизации: путь к приватному ключу, парольная фраза, либо пароль (маскированный).
57
- - Директории: Удалённый каталог проекта (с кнопкой вызова SFTP браузера) и **Локальная папка зеркала** (с подсказкой назначения).
58
- - Кнопки: «⚡ Проверить связь (Ping)», «Отмена», «Сохранить».
59
- - Результат пинга: отображение Latency: X ms | OS: Y или понятный текст ошибки.
60
- 3. **Секция 3: Диагностика активного подключения**:
61
- - Четырёхколоночная сетка метрик активного сервера (Хост, Удалённая папка, Метод авторизации, Статус ядра).
62
- 4. **Секция 4: Синхронизация зеркала (Mirror Sync)**:
63
- - Кнопки Pull (Remote -> Local) и Push (Local -> Remote) с индикацией процесса, результатом и предупреждением при конфликтах.
64
- 5. **Секция 5: SSH-туннели (Port Forwarding)**:
65
- - Таблица активных туннелей (ID, Локальный порт -> Удалённый порт) с кнопкой закрытия.
66
- - Форма создания нового туннеля (Local Port, Remote Port, кнопка «Открыть туннель»).
67
-
68
- ## Security & Reliability
69
- - Защита всех мутирующих эндпоинтов проверкой isTrustedSettingsRequest(req) (защита от cross-site атак).
70
- - Безопасное чтение Cordis сервисов через .get() с fallback.
71
- - Изоляция жизненного цикла стримов в TunnelService (прослушивание сокетных и потоковых ошибок).
72
-
73
- ### v0.3.3 Client Architecture Modularization (Refs: #14)
74
- - Client-side code decomposed into modular ES components under `src/client/`:
75
- - `src/client/styles.js` — CSS styling with DSH theme variables
76
- - `src/client/locales.js` — English and Chinese translation dictionaries
77
- - `src/client/env-editor.js` — Remote .env manager tab component
78
- - `src/client/docker.js` — Container inspector tab component
79
- - `src/client/terminal.js` — Web terminal tab component
80
- - `src/client/file-browser.js` — Remote file explorer tab component
81
- - `src/client/settings-view.js` — Profiles, tunnels, and sync settings component
82
- - `src/client/updater-section.js` — One-click updater section component
83
- - `src/client/plugin-card.js` — Plugin card and status chip components
84
- - `src/client/entry.js` — Cordis client plugin apply and slot registration
85
- - Added native builder `scripts/build-client.mjs` hooked into `prepack` and `npm test`.
86
-
87
- ### v0.3.6 Model Tools JSON Schema Standardization (Refs: #33)
88
- - Все 9 инструментов (`remote_exec`, `remote_fs`, `remote_sync`, `remote_tunnel`, `remote_docker`, `remote_service`, `remote_transfer`, `remote_diagnose`, `remote_env`) переведены на компиляцию через `defineTool()` из `@deepseek-ai/dsh-tools`.
89
- - Параметры инструментов компилируются в строгий JSON Schema с корнем `{ type: 'object', properties: ..., required: [...] }`.
90
- - Предотвращены ошибки валидации схем в OpenAI-совместимых провайдерах (`Invalid schema: schema must be a JSON Schema of 'type: "object"', got 'type: null'`).
91
- - Схема вывода стандартизирована с явным `{ type: 'object', additionalProperties: true }`.