tabby-sftp-ui 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -126,6 +126,12 @@ Then restart Tabby.
126
126
 
127
127
  ### Changelog
128
128
 
129
+ - See [`CHANGELOG.md`](CHANGELOG.md)
130
+
131
+ - **0.2.1**
132
+ - Fix: New Folder/Rename input dialog
133
+ - Fix: Robust remote file edit sync-back
134
+ - Feature: OS drag-and-drop into panes
129
135
  - **0.2.0**
130
136
  - UI: removed the main toolbar SFTP icon next to Settings (terminal button remains).
131
137
  - **0.1.0**
package/dist/index.js CHANGED
@@ -229,12 +229,14 @@ __webpack_require__.r(__webpack_exports__);
229
229
  /* harmony import */ var fs_promises__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs_promises__WEBPACK_IMPORTED_MODULE_2__);
230
230
  /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! fs */ "fs");
231
231
  /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_3__);
232
- /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @angular/core */ "@angular/core");
233
- /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_4__);
234
- /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! tabby-core */ "tabby-core");
235
- /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_5__);
236
- /* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
237
- /* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
232
+ /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! crypto */ "crypto");
233
+ /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_4__);
234
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @angular/core */ "@angular/core");
235
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_5__);
236
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! tabby-core */ "tabby-core");
237
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_6__);
238
+ /* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
239
+ /* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
238
240
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
239
241
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
240
242
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -252,7 +254,8 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
252
254
 
253
255
 
254
256
 
255
- let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_5__.BaseTabComponent {
257
+
258
+ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_6__.BaseTabComponent {
256
259
  constructor(injector, sftp, profilesService, app) {
257
260
  // Tabby runtime BaseTabComponent expects Injector in constructor, but typings in this SDK may differ.
258
261
  // @ts-expect-error runtime-compatible super(injector)
@@ -306,6 +309,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
306
309
  this.deleteConfirmText = '';
307
310
  this.pendingLocalDelete = [];
308
311
  this.pendingRemoteDelete = [];
312
+ this.inputDialogVisible = false;
313
+ this.inputDialogTitle = '';
314
+ this.inputDialogPlaceholder = '';
315
+ this.inputDialogValue = '';
316
+ this.inputDialogMode = null;
317
+ this.inputDialogTargetPath = null;
318
+ this.inputDialogRemotePath = null;
309
319
  this.openedRemoteFiles = new Map();
310
320
  this.localPathPresets = [];
311
321
  this.localFavorites = [];
@@ -314,7 +324,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
314
324
  this.localMenuX = 0;
315
325
  this.localMenuY = 0;
316
326
  this.localMenuItems = [];
317
- this.platform = injector.get(tabby_core__WEBPACK_IMPORTED_MODULE_5__.PlatformService);
327
+ this.platform = injector.get(tabby_core__WEBPACK_IMPORTED_MODULE_6__.PlatformService);
318
328
  // build local path presets (similar to Termius quick locations)
319
329
  const home = os__WEBPACK_IMPORTED_MODULE_1__.homedir();
320
330
  this.localPathPresets.push({ id: 'home', label: 'Home', path: home });
@@ -621,6 +631,25 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
621
631
  if (!this.sftpSession) {
622
632
  return;
623
633
  }
634
+ // Drag & drop from OS file manager (Explorer/Finder) into the remote pane
635
+ const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
636
+ if (dtFiles.length) {
637
+ const paths = dtFiles
638
+ .map(f => f.path)
639
+ .filter((p) => Boolean(p));
640
+ if (paths.length) {
641
+ try {
642
+ for (const p of paths) {
643
+ await this.uploadLocalPathToRemote(this.remotePath, p);
644
+ }
645
+ await this.refreshRemote();
646
+ }
647
+ catch (e) {
648
+ console.error('[SFTP-UI] Upload from OS drop failed', e);
649
+ }
650
+ }
651
+ return;
652
+ }
624
653
  const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
625
654
  if (!raw) {
626
655
  return;
@@ -637,7 +666,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
637
666
  }
638
667
  try {
639
668
  const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
640
- const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileUpload(payload.fullPath);
669
+ const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
641
670
  this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
642
671
  await this.sftpSession.upload(targetRemotePath, upload);
643
672
  await this.refreshRemote();
@@ -649,6 +678,25 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
649
678
  async onDropOnLocal(ev) {
650
679
  ev.preventDefault();
651
680
  this.localDropActive = false;
681
+ // Drag & drop from OS file manager into the local pane (copy into current local folder)
682
+ const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
683
+ if (dtFiles.length) {
684
+ const paths = dtFiles
685
+ .map(f => f.path)
686
+ .filter((p) => Boolean(p));
687
+ if (paths.length) {
688
+ try {
689
+ for (const p of paths) {
690
+ await this.copyLocalPathIntoLocalDir(this.localPath, p);
691
+ }
692
+ await this.refreshLocal();
693
+ }
694
+ catch (e) {
695
+ console.error('[SFTP-UI] Local copy from OS drop failed', e);
696
+ }
697
+ }
698
+ return;
699
+ }
652
700
  const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
653
701
  if (!raw) {
654
702
  return;
@@ -668,7 +716,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
668
716
  if (!this.sftpSession) {
669
717
  throw new Error('Not connected');
670
718
  }
671
- const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
719
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
672
720
  this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
673
721
  await this.sftpSession.download(payload.remotePath, dl);
674
722
  await this.refreshLocal();
@@ -677,6 +725,51 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
677
725
  console.error('[SFTP-UI] Download failed', e);
678
726
  }
679
727
  }
728
+ async uploadLocalPathToRemote(remoteDir, localPath) {
729
+ if (!this.sftpSession) {
730
+ return;
731
+ }
732
+ const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(localPath).catch(() => null);
733
+ if (!st) {
734
+ return;
735
+ }
736
+ const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(localPath);
737
+ const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, baseName);
738
+ if (st.isDirectory()) {
739
+ // Ensure destination folder exists, then recursively upload children
740
+ try {
741
+ await this.sftpSession.mkdir(remoteTarget);
742
+ }
743
+ catch {
744
+ // ignore (might already exist)
745
+ }
746
+ const children = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(localPath);
747
+ for (const child of children) {
748
+ await this.uploadLocalPathToRemote(remoteTarget, path__WEBPACK_IMPORTED_MODULE_0__.join(localPath, child));
749
+ }
750
+ return;
751
+ }
752
+ const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(localPath);
753
+ this.trackTransfer(upload, 'upload', remoteTarget, localPath);
754
+ await this.sftpSession.upload(remoteTarget, upload);
755
+ }
756
+ async copyLocalPathIntoLocalDir(destDir, srcPath) {
757
+ const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(srcPath).catch(() => null);
758
+ if (!st) {
759
+ return;
760
+ }
761
+ const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(srcPath);
762
+ const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(destDir, baseName);
763
+ if (st.isDirectory()) {
764
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(destPath, { recursive: true });
765
+ const children = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(srcPath);
766
+ for (const child of children) {
767
+ await this.copyLocalPathIntoLocalDir(destPath, path__WEBPACK_IMPORTED_MODULE_0__.join(srcPath, child));
768
+ }
769
+ return;
770
+ }
771
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.copyFile(srcPath, destPath);
772
+ }
680
773
  getFilteredLocalEntries() {
681
774
  const entriesRef = this.localEntries;
682
775
  const filter = this.localFilter;
@@ -1215,18 +1308,17 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1215
1308
  this.localMenuVisible = false;
1216
1309
  }
1217
1310
  localRename() {
1218
- if (this.selectedLocal.length !== 1 || !this.localActionName?.trim()) {
1311
+ if (this.selectedLocal.length !== 1) {
1219
1312
  return;
1220
1313
  }
1221
1314
  const entry = this.selectedLocal[0];
1222
- const name = this.localActionName.trim();
1223
- if (name === entry.name) {
1224
- return;
1225
- }
1226
- const target = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, name);
1227
- void fs_promises__WEBPACK_IMPORTED_MODULE_2__.rename(entry.fullPath, target)
1228
- .then(() => this.refreshLocal())
1229
- .catch(e => console.error('[SFTP-UI] Local rename failed', e));
1315
+ this.openInputDialog({
1316
+ mode: 'local-rename',
1317
+ title: 'Rename (local)',
1318
+ placeholder: 'New name',
1319
+ value: entry.name,
1320
+ targetPath: entry.fullPath,
1321
+ });
1230
1322
  }
1231
1323
  localDelete() {
1232
1324
  if (!this.selectedLocal.length) {
@@ -1239,14 +1331,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1239
1331
  this.deleteConfirmVisible = true;
1240
1332
  }
1241
1333
  localNewFolder() {
1242
- const name = (this.localActionName || 'New folder').trim();
1243
- if (!name) {
1244
- return;
1245
- }
1246
- const target = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, name);
1247
- void fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(target, { recursive: true })
1248
- .then(() => this.refreshLocal())
1249
- .catch(e => console.error('[SFTP-UI] Local mkdir failed', e));
1334
+ this.openInputDialog({
1335
+ mode: 'local-new-folder',
1336
+ title: 'New folder (local)',
1337
+ placeholder: 'Folder name',
1338
+ value: 'New folder',
1339
+ targetPath: this.localPath,
1340
+ });
1250
1341
  }
1251
1342
  localEditPermissions() {
1252
1343
  if (this.selectedLocal.length !== 1 || !this.localActionPerms?.trim()) {
@@ -1268,18 +1359,18 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1268
1359
  }
1269
1360
  }
1270
1361
  remoteRename() {
1271
- if (this.selectedRemote.length !== 1 || !this.remoteActionName?.trim() || !this.sftpSession) {
1362
+ if (this.selectedRemote.length !== 1 || !this.sftpSession) {
1272
1363
  return;
1273
1364
  }
1274
1365
  const entry = this.selectedRemote[0];
1275
- const name = this.remoteActionName.trim();
1276
- if (name === entry.name) {
1277
- return;
1278
- }
1279
- const target = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, name);
1280
- void this.sftpSession.rename(entry.fullPath, target)
1281
- .then(() => this.refreshRemote())
1282
- .catch(e => console.error('[SFTP-UI] Remote rename failed', e));
1366
+ this.openInputDialog({
1367
+ mode: 'remote-rename',
1368
+ title: 'Rename (remote)',
1369
+ placeholder: 'New name',
1370
+ value: entry.name,
1371
+ remotePath: entry.fullPath,
1372
+ targetPath: this.remotePath,
1373
+ });
1283
1374
  }
1284
1375
  remoteDelete() {
1285
1376
  if (!this.selectedRemote.length || !this.sftpSession) {
@@ -1295,14 +1386,87 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1295
1386
  if (!this.sftpSession) {
1296
1387
  return;
1297
1388
  }
1298
- const name = (this.remoteActionName || 'New folder').trim();
1299
- if (!name) {
1389
+ this.openInputDialog({
1390
+ mode: 'remote-new-folder',
1391
+ title: 'New folder (remote)',
1392
+ placeholder: 'Folder name',
1393
+ value: 'New folder',
1394
+ targetPath: this.remotePath,
1395
+ });
1396
+ }
1397
+ openInputDialog(opts) {
1398
+ this.inputDialogMode = opts.mode;
1399
+ this.inputDialogTitle = opts.title;
1400
+ this.inputDialogPlaceholder = opts.placeholder;
1401
+ this.inputDialogValue = opts.value;
1402
+ this.inputDialogTargetPath = opts.targetPath;
1403
+ this.inputDialogRemotePath = opts.remotePath ?? null;
1404
+ this.inputDialogVisible = true;
1405
+ }
1406
+ cancelInputDialog() {
1407
+ this.inputDialogVisible = false;
1408
+ this.inputDialogMode = null;
1409
+ this.inputDialogTitle = '';
1410
+ this.inputDialogPlaceholder = '';
1411
+ this.inputDialogValue = '';
1412
+ this.inputDialogTargetPath = null;
1413
+ this.inputDialogRemotePath = null;
1414
+ }
1415
+ async confirmInputDialog() {
1416
+ if (!this.inputDialogVisible || !this.inputDialogMode) {
1300
1417
  return;
1301
1418
  }
1302
- const target = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, name);
1303
- void this.sftpSession.mkdir(target)
1304
- .then(() => this.refreshRemote())
1305
- .catch(e => console.error('[SFTP-UI] Remote mkdir failed', e));
1419
+ const mode = this.inputDialogMode;
1420
+ const value = this.inputDialogValue.trim();
1421
+ const targetPath = this.inputDialogTargetPath;
1422
+ const remotePath = this.inputDialogRemotePath;
1423
+ this.cancelInputDialog();
1424
+ if (!value || !targetPath) {
1425
+ return;
1426
+ }
1427
+ try {
1428
+ if (mode === 'local-new-folder') {
1429
+ const dir = targetPath;
1430
+ const folderPath = path__WEBPACK_IMPORTED_MODULE_0__.join(dir, value);
1431
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(folderPath, { recursive: true });
1432
+ await this.refreshLocal();
1433
+ return;
1434
+ }
1435
+ if (mode === 'local-rename') {
1436
+ const from = targetPath;
1437
+ const to = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, value);
1438
+ if (path__WEBPACK_IMPORTED_MODULE_0__.basename(from) === value) {
1439
+ return;
1440
+ }
1441
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.rename(from, to);
1442
+ await this.refreshLocal();
1443
+ return;
1444
+ }
1445
+ if (mode === 'remote-new-folder') {
1446
+ if (!this.sftpSession) {
1447
+ return;
1448
+ }
1449
+ const dir = targetPath;
1450
+ const folderPath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(dir, value);
1451
+ await this.sftpSession.mkdir(folderPath);
1452
+ await this.refreshRemote();
1453
+ return;
1454
+ }
1455
+ if (mode === 'remote-rename') {
1456
+ if (!this.sftpSession || !remotePath) {
1457
+ return;
1458
+ }
1459
+ const to = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, value);
1460
+ if (path__WEBPACK_IMPORTED_MODULE_0__.posix.basename(remotePath) === value) {
1461
+ return;
1462
+ }
1463
+ await this.sftpSession.rename(remotePath, to);
1464
+ await this.refreshRemote();
1465
+ }
1466
+ }
1467
+ catch (e) {
1468
+ console.error('[SFTP-UI] Input dialog action failed', e);
1469
+ }
1306
1470
  }
1307
1471
  remoteEditPermissions() {
1308
1472
  if (this.selectedRemote.length !== 1 || !this.remoteActionPerms?.trim() || !this.sftpSession) {
@@ -1332,7 +1496,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1332
1496
  continue;
1333
1497
  }
1334
1498
  const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, entry.name);
1335
- const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(targetLocalPath, entry.mode, entry.size);
1499
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, entry.mode, entry.size);
1336
1500
  this.trackTransfer(dl, 'download', entry.fullPath, targetLocalPath);
1337
1501
  void this.sftpSession.download(entry.fullPath, dl)
1338
1502
  .then(() => this.refreshLocal())
@@ -1503,6 +1667,18 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1503
1667
  }
1504
1668
  }
1505
1669
  onKeyDown(event) {
1670
+ if (event.key === 'Escape') {
1671
+ if (this.inputDialogVisible) {
1672
+ event.preventDefault();
1673
+ this.cancelInputDialog();
1674
+ return;
1675
+ }
1676
+ if (this.deleteConfirmVisible) {
1677
+ event.preventDefault();
1678
+ this.cancelDelete();
1679
+ return;
1680
+ }
1681
+ }
1506
1682
  if (event.key === 'Delete' || event.key === 'Backspace') {
1507
1683
  event.preventDefault();
1508
1684
  if (this.selectedRemote.length) {
@@ -1675,7 +1851,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1675
1851
  try {
1676
1852
  const tmpRoot = path__WEBPACK_IMPORTED_MODULE_0__.join(os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'tabby-sftp-ui');
1677
1853
  await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(tmpRoot, { recursive: true });
1678
- const localPath = path__WEBPACK_IMPORTED_MODULE_0__.join(tmpRoot, entry.name);
1854
+ const hash = crypto__WEBPACK_IMPORTED_MODULE_4__.createHash('sha1').update(entry.fullPath).digest('hex').slice(0, 10);
1855
+ const safeName = entry.name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
1856
+ const localPath = path__WEBPACK_IMPORTED_MODULE_0__.join(tmpRoot, `${hash}-${safeName}`);
1679
1857
  // если уже есть watcher на этот файл – закроем его и перезапишем
1680
1858
  const existing = this.openedRemoteFiles.get(localPath);
1681
1859
  if (existing?.watcher) {
@@ -1686,22 +1864,72 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1686
1864
  // ignore
1687
1865
  }
1688
1866
  }
1689
- const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(localPath, entry.mode, entry.size);
1867
+ if (existing?.debounceTimer != null) {
1868
+ try {
1869
+ clearTimeout(existing.debounceTimer);
1870
+ }
1871
+ catch {
1872
+ // ignore
1873
+ }
1874
+ }
1875
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(localPath, entry.mode, entry.size);
1690
1876
  this.trackTransfer(dl, 'download', entry.fullPath, localPath);
1691
1877
  await this.sftpSession.download(entry.fullPath, dl);
1692
1878
  // настроим наблюдение за изменениями локального файла
1879
+ const schedule = () => this.scheduleSyncBackRemoteFile(localPath);
1693
1880
  const watcher = fs__WEBPACK_IMPORTED_MODULE_3__.watch(localPath, { persistent: false }, (eventType) => {
1694
- if (eventType === 'change') {
1695
- void this.syncBackRemoteFile(localPath);
1881
+ // Many editors save atomically (rename) or emit multiple change events.
1882
+ if (eventType === 'change' || eventType === 'rename') {
1883
+ schedule();
1696
1884
  }
1697
1885
  });
1698
- this.openedRemoteFiles.set(localPath, { remotePath: entry.fullPath, mode: entry.mode, watcher });
1886
+ this.openedRemoteFiles.set(localPath, {
1887
+ remotePath: entry.fullPath,
1888
+ mode: entry.mode,
1889
+ watcher,
1890
+ debounceTimer: null,
1891
+ syncing: false,
1892
+ pending: false,
1893
+ lastUploadedSignature: null,
1894
+ });
1699
1895
  this.platform.openPath(localPath);
1700
1896
  }
1701
1897
  catch (e) {
1702
1898
  console.error('[SFTP-UI] Open remote file failed', e);
1703
1899
  }
1704
1900
  }
1901
+ scheduleSyncBackRemoteFile(localPath) {
1902
+ const info = this.openedRemoteFiles.get(localPath);
1903
+ if (!info) {
1904
+ return;
1905
+ }
1906
+ if (info.debounceTimer != null) {
1907
+ clearTimeout(info.debounceTimer);
1908
+ }
1909
+ // Debounce a burst of editor save events
1910
+ info.debounceTimer = window.setTimeout(() => {
1911
+ info.debounceTimer = null;
1912
+ void this.syncBackRemoteFile(localPath);
1913
+ }, 650);
1914
+ }
1915
+ async waitForStableLocalFile(localPath) {
1916
+ // Wait until the file stops changing (editors often write in multiple passes)
1917
+ let last = null;
1918
+ for (let i = 0; i < 10; i++) {
1919
+ const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(localPath).catch(() => null);
1920
+ if (!st || !st.isFile()) {
1921
+ return null;
1922
+ }
1923
+ const cur = { size: st.size, mtimeMs: st.mtimeMs };
1924
+ if (last && cur.size === last.size && cur.mtimeMs === last.mtimeMs) {
1925
+ // stable for one interval
1926
+ return cur;
1927
+ }
1928
+ last = cur;
1929
+ await new Promise(resolve => setTimeout(resolve, 180));
1930
+ }
1931
+ return last;
1932
+ }
1705
1933
  async syncBackRemoteFile(localPath) {
1706
1934
  if (!this.sftpSession || !this.connected) {
1707
1935
  return;
@@ -1710,25 +1938,46 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1710
1938
  if (!info) {
1711
1939
  return;
1712
1940
  }
1941
+ if (info.syncing) {
1942
+ info.pending = true;
1943
+ return;
1944
+ }
1945
+ info.syncing = true;
1713
1946
  try {
1714
- const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileUpload(localPath);
1947
+ const stable = await this.waitForStableLocalFile(localPath);
1948
+ if (!stable) {
1949
+ return;
1950
+ }
1951
+ const signature = `${stable.size}:${stable.mtimeMs}`;
1952
+ if (info.lastUploadedSignature === signature) {
1953
+ return;
1954
+ }
1955
+ const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(localPath);
1715
1956
  this.trackTransfer(upload, 'upload', info.remotePath, localPath);
1716
1957
  await this.sftpSession.upload(info.remotePath, upload);
1958
+ info.lastUploadedSignature = signature;
1717
1959
  await this.refreshRemote();
1718
1960
  }
1719
1961
  catch (e) {
1720
1962
  console.error('[SFTP-UI] Sync-back remote file failed', e);
1721
1963
  }
1964
+ finally {
1965
+ info.syncing = false;
1966
+ if (info.pending) {
1967
+ info.pending = false;
1968
+ this.scheduleSyncBackRemoteFile(localPath);
1969
+ }
1970
+ }
1722
1971
  }
1723
1972
  };
1724
1973
  __decorate([
1725
- (0,_angular_core__WEBPACK_IMPORTED_MODULE_4__.HostListener)('document:click'),
1974
+ (0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.HostListener)('document:click'),
1726
1975
  __metadata("design:type", Function),
1727
1976
  __metadata("design:paramtypes", []),
1728
1977
  __metadata("design:returntype", void 0)
1729
1978
  ], SftpManagerTabComponent.prototype, "onDocumentClick", null);
1730
1979
  SftpManagerTabComponent = __decorate([
1731
- (0,_angular_core__WEBPACK_IMPORTED_MODULE_4__.Component)({
1980
+ (0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
1732
1981
  selector: 'tabby-sftp-manager-tab',
1733
1982
  template: `
1734
1983
  <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
@@ -1842,7 +2091,7 @@ SftpManagerTabComponent = __decorate([
1842
2091
  <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
1843
2092
  </div>
1844
2093
  <div class="action-buttons">
1845
- <button (click)="localRename()" [disabled]="selectedLocal.length !== 1 || !localActionName">Rename</button>
2094
+ <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
1846
2095
  <button (click)="refreshLocal()">Refresh</button>
1847
2096
  <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
1848
2097
  <button (click)="localNewFolder()">New Folder</button>
@@ -1943,7 +2192,7 @@ SftpManagerTabComponent = __decorate([
1943
2192
  <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
1944
2193
  </div>
1945
2194
  <div class="action-buttons">
1946
- <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1 || !remoteActionName">Rename</button>
2195
+ <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
1947
2196
  <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
1948
2197
  <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
1949
2198
  <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
@@ -1991,6 +2240,22 @@ SftpManagerTabComponent = __decorate([
1991
2240
  </div>
1992
2241
  </div>
1993
2242
 
2243
+ <div class="delete-overlay" *ngIf="inputDialogVisible">
2244
+ <div class="delete-dialog" (click)="$event.stopPropagation()">
2245
+ <div class="delete-text">{{ inputDialogTitle }}</div>
2246
+ <input
2247
+ class="dialog-input"
2248
+ [(ngModel)]="inputDialogValue"
2249
+ [placeholder]="inputDialogPlaceholder"
2250
+ (keyup.enter)="confirmInputDialog()"
2251
+ />
2252
+ <div class="delete-buttons">
2253
+ <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2254
+ <button (click)="cancelInputDialog()">Cancel</button>
2255
+ </div>
2256
+ </div>
2257
+ </div>
2258
+
1994
2259
  <div
1995
2260
  class="local-menu"
1996
2261
  *ngIf="localMenuVisible"
@@ -2070,6 +2335,7 @@ SftpManagerTabComponent = __decorate([
2070
2335
  .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2071
2336
  .delete-dialog { min-width: 260px; max-width: 360px; padding: 14px 16px; border-radius: 10px; background: rgba(20,20,20,0.96); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 18px 45px rgba(0,0,0,0.75); display: flex; flex-direction: column; gap: 10px; }
2072
2337
  .delete-text { font-size: 13px; }
2338
+ .dialog-input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 13px; }
2073
2339
  .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2074
2340
  .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2075
2341
  .local-menu { position: absolute; min-width: 180px; max-width: 260px; max-height: 260px; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: rgba(18,18,22,0.98); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 45px rgba(0,0,0,0.8); z-index: 30; backdrop-filter: blur(12px); }
@@ -2077,10 +2343,10 @@ SftpManagerTabComponent = __decorate([
2077
2343
  .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2078
2344
  `],
2079
2345
  }),
2080
- __metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_4__.Injector,
2081
- _sftp_service__WEBPACK_IMPORTED_MODULE_7__.SftpConnectionService,
2082
- tabby_core__WEBPACK_IMPORTED_MODULE_5__.ProfilesService,
2083
- tabby_core__WEBPACK_IMPORTED_MODULE_5__.AppService])
2346
+ __metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
2347
+ _sftp_service__WEBPACK_IMPORTED_MODULE_8__.SftpConnectionService,
2348
+ tabby_core__WEBPACK_IMPORTED_MODULE_6__.ProfilesService,
2349
+ tabby_core__WEBPACK_IMPORTED_MODULE_6__.AppService])
2084
2350
  ], SftpManagerTabComponent);
2085
2351
 
2086
2352
 
@@ -2314,6 +2580,16 @@ SftpConnectionService = __decorate([
2314
2580
 
2315
2581
 
2316
2582
 
2583
+ /***/ },
2584
+
2585
+ /***/ "crypto"
2586
+ /*!*************************!*\
2587
+ !*** external "crypto" ***!
2588
+ \*************************/
2589
+ (module) {
2590
+
2591
+ module.exports = require("crypto");
2592
+
2317
2593
  /***/ },
2318
2594
 
2319
2595
  /***/ "fs/promises"