tabby-sftp-ui 0.2.5 → 0.2.7

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/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  (function webpackUniversalModuleDefinition(root, factory) {
2
2
  if(typeof exports === 'object' && typeof module === 'object')
3
- module.exports = factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), require("fs"));
3
+ module.exports = factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs"));
4
4
  else if(typeof define === 'function' && define.amd)
5
- define(["@angular/common", "@angular/forms", "@angular/core", "tabby-core", "tabby-terminal", "path", "fs"], factory);
5
+ define(["@angular/common", "@angular/forms", "@angular/core", "tabby-core", "tabby-terminal", "path", "electron", "fs"], factory);
6
6
  else {
7
- var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), require("fs")) : factory(root["@angular/common"], root["@angular/forms"], root["@angular/core"], root["tabby-core"], root["tabby-terminal"], root["path"], root["fs"]);
7
+ var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), (function webpackLoadOptionalExternalModule() { try { return require("electron"); } catch(e) {} }()), require("fs")) : factory(root["@angular/common"], root["@angular/forms"], root["@angular/core"], root["tabby-core"], root["tabby-terminal"], root["path"], root["electron"], root["fs"]);
8
8
  for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
9
9
  }
10
- })(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_fs__) => {
10
+ })(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_electron__, __WEBPACK_EXTERNAL_MODULE_fs__) => {
11
11
  return /******/ (() => { // webpackBootstrap
12
12
  /******/ "use strict";
13
13
  /******/ var __webpack_modules__ = ({
@@ -267,6 +267,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
267
267
  this.app = app;
268
268
  // injected from the SSH tab when opened via SFTP-UI button
269
269
  this.sshSession = null;
270
+ this.shellSession = null;
270
271
  this.profile = null;
271
272
  this.recoveredStub = false;
272
273
  this.connecting = false;
@@ -281,6 +282,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
281
282
  this.remotePath = '/';
282
283
  this.remoteEntries = [];
283
284
  this.sftpSession = null;
285
+ this.connectPromise = null;
284
286
  this.localDropActive = false;
285
287
  this.remoteDropActive = false;
286
288
  this.transfers = [];
@@ -368,11 +370,32 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
368
370
  this.remotePathInput = this.remotePath;
369
371
  this.localPathInput = this.localPath;
370
372
  if (this.sshSession) {
371
- void this.connect();
373
+ void this.waitForConnection();
372
374
  }
373
375
  this.loadRecentProfiles();
374
376
  }
375
377
  async connect() {
378
+ if (this.connected) {
379
+ return;
380
+ }
381
+ if (this.connectPromise) {
382
+ return this.connectPromise;
383
+ }
384
+ this.connectPromise = this.connectInternal();
385
+ try {
386
+ await this.connectPromise;
387
+ }
388
+ finally {
389
+ this.connectPromise = null;
390
+ }
391
+ }
392
+ async waitForConnection() {
393
+ if (this.connected) {
394
+ return;
395
+ }
396
+ await this.connect();
397
+ }
398
+ async connectInternal() {
376
399
  if (this.connecting || this.connected) {
377
400
  return;
378
401
  }
@@ -384,17 +407,30 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
384
407
  try {
385
408
  this.sftpSession = await this.sftp.openFromSSHSession(this.sshSession);
386
409
  this.connected = true;
387
- this.remotePath = this.getDefaultRemotePath();
388
- this.remotePathInput = this.remotePath;
410
+ await this.initializeRemotePath();
389
411
  await this.refreshRemote();
390
412
  }
391
413
  catch (e) {
392
414
  console.error('[SFTP-UI] SFTP connection failed', e);
415
+ this.connected = false;
416
+ this.sftpSession = null;
393
417
  }
394
418
  finally {
395
419
  this.connecting = false;
396
420
  }
397
421
  }
422
+ async initializeRemotePath() {
423
+ this.remotePath = await this.resolveDefaultRemotePath();
424
+ this.remotePathInput = this.remotePath;
425
+ if (await this.ensureRemoteDirectoryReady()) {
426
+ return;
427
+ }
428
+ // Shell CWD may not be ready yet when SFTP-UI opens immediately after SSH connect.
429
+ await new Promise(resolve => setTimeout(resolve, 500));
430
+ this.remotePath = await this.resolveDefaultRemotePath();
431
+ this.remotePathInput = this.remotePath;
432
+ await this.ensureRemoteDirectoryReady();
433
+ }
398
434
  async disconnect() {
399
435
  this.sftpSession = null;
400
436
  this.connected = false;
@@ -413,14 +449,26 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
413
449
  }
414
450
  }
415
451
  remoteUp() {
416
- if (!this.connected || this.remotePath === '/') {
452
+ if (!this.canRemoteUp()) {
417
453
  return;
418
454
  }
419
- const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(this.remotePath);
420
- this.remotePath = next === '.' ? '/' : next;
455
+ const next = this.remoteParentPath(this.remotePath);
456
+ this.remotePath = next;
421
457
  this.remotePathInput = this.remotePath;
422
458
  void this.refreshRemote();
423
459
  }
460
+ canRemoteUp() {
461
+ if (!this.connected) {
462
+ return false;
463
+ }
464
+ if (this.remotePath === '/') {
465
+ return false;
466
+ }
467
+ if (this.isWindowsSftpPath(this.remotePath)) {
468
+ return !/^\/[A-Za-z]:\/?$/.test(this.remotePath);
469
+ }
470
+ return true;
471
+ }
424
472
  async refreshLocal() {
425
473
  try {
426
474
  const names = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(this.localPath);
@@ -484,6 +532,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
484
532
  }
485
533
  onDragOver(ev) {
486
534
  ev.preventDefault();
535
+ if (ev.dataTransfer) {
536
+ ev.dataTransfer.dropEffect = 'copy';
537
+ }
487
538
  }
488
539
  onLocalMouseDown(entry, event) {
489
540
  if (event.button === 2) {
@@ -633,17 +684,19 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
633
684
  async onDropOnRemote(ev) {
634
685
  ev.preventDefault();
635
686
  this.remoteDropActive = false;
636
- if (!this.connected) {
687
+ const captured = this.captureDropData(ev);
688
+ await this.waitForConnection();
689
+ if (!this.connected || !this.sftpSession) {
637
690
  return;
638
691
  }
639
- if (!this.sftpSession) {
692
+ if (!(await this.ensureRemoteDirectoryReady())) {
693
+ console.error('[SFTP-UI] Remote path is not ready for upload');
640
694
  return;
641
695
  }
642
696
  // Drag & drop from OS file manager (Explorer/Finder) into the remote pane
643
- const osPaths = this.getDroppedOsPaths(ev);
644
- if (osPaths.length) {
697
+ if (captured.osPaths.length) {
645
698
  try {
646
- for (const p of osPaths) {
699
+ for (const p of captured.osPaths) {
647
700
  const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
648
701
  const existing = this.remoteEntries.find(e => e.name === baseName);
649
702
  if (existing) {
@@ -663,73 +716,85 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
663
716
  }
664
717
  return;
665
718
  }
666
- // Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
667
- try {
668
- const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
669
- if (dirUpload && this.sftpSession) {
670
- await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
671
- await this.refreshRemote();
672
- return;
719
+ const raw = captured.internalPayload;
720
+ if (raw) {
721
+ let payload;
722
+ try {
723
+ payload = JSON.parse(raw);
673
724
  }
674
- }
675
- catch (e) {
676
- console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
677
- }
678
- const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
679
- if (!raw) {
680
- return;
681
- }
682
- let payload;
683
- try {
684
- payload = JSON.parse(raw);
685
- }
686
- catch {
687
- return;
688
- }
689
- try {
690
- if (payload.kind === 'local-file') {
691
- const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
692
- const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
693
- if (existsOnRemote) {
694
- const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
695
- if (!ok) {
696
- return;
697
- }
698
- await this.deleteRemotePathRecursive(targetRemotePath);
699
- }
700
- const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
701
- this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
702
- await this.sftpSession.upload(targetRemotePath, upload);
703
- await this.refreshRemote();
704
- return;
725
+ catch {
726
+ payload = null;
705
727
  }
706
- if (payload.kind === 'local-paths') {
707
- for (const p of payload.paths) {
708
- const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, p.name);
709
- const existsOnRemote = this.remoteEntries.some(e => e.name === p.name);
728
+ try {
729
+ if (payload && payload.kind === 'local-file') {
730
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
731
+ const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
710
732
  if (existsOnRemote) {
711
- const ok = await this.showReplaceConfirm(`Replace existing "${p.name}" on remote?`);
733
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
712
734
  if (!ok) {
713
- continue;
735
+ return;
714
736
  }
715
737
  await this.deleteRemotePathRecursive(targetRemotePath);
716
738
  }
717
- // uploadLocalPathToRemote handles both files and directories
718
- await this.uploadLocalPathToRemote(this.remotePath, p.fullPath);
739
+ const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
740
+ this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
741
+ await this.sftpSession.upload(targetRemotePath, upload);
742
+ await this.refreshRemote();
743
+ return;
719
744
  }
745
+ if (payload && payload.kind === 'local-paths') {
746
+ for (const p of payload.paths) {
747
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, p.name);
748
+ const existsOnRemote = this.remoteEntries.some(e => e.name === p.name);
749
+ if (existsOnRemote) {
750
+ const ok = await this.showReplaceConfirm(`Replace existing "${p.name}" on remote?`);
751
+ if (!ok) {
752
+ continue;
753
+ }
754
+ await this.deleteRemotePathRecursive(targetRemotePath);
755
+ }
756
+ // uploadLocalPathToRemote handles both files and directories
757
+ await this.uploadLocalPathToRemote(this.remotePath, p.fullPath);
758
+ }
759
+ await this.refreshRemote();
760
+ return;
761
+ }
762
+ }
763
+ catch (e) {
764
+ console.error('[SFTP-UI] Upload failed', e);
765
+ }
766
+ }
767
+ // Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
768
+ try {
769
+ const dirUpload = captured.dirUploadPromise ? await captured.dirUploadPromise : null;
770
+ const childCount = this.countDirectoryUploadItems(dirUpload);
771
+ if (dirUpload && childCount > 0 && this.sftpSession) {
772
+ await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
720
773
  await this.refreshRemote();
721
774
  return;
722
775
  }
723
776
  }
724
777
  catch (e) {
725
- console.error('[SFTP-UI] Upload failed', e);
778
+ console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
779
+ }
780
+ if (captured.files.length > 0) {
781
+ try {
782
+ const uploaded = await this.uploadDroppedFilesToRemote(captured.files, this.remotePath);
783
+ if (uploaded > 0) {
784
+ await this.refreshRemote();
785
+ }
786
+ }
787
+ catch (e) {
788
+ console.error('[SFTP-UI] Blob upload failed', e);
789
+ }
726
790
  }
727
791
  }
728
792
  async onDropOnLocal(ev) {
729
793
  ev.preventDefault();
730
794
  this.localDropActive = false;
795
+ const captured = this.captureDropData(ev);
731
796
  // 1) Tabby's internal drag (remote -> local download)
732
- const rawInternal = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
797
+ const rawInternal = captured.internalPayload;
733
798
  if (rawInternal) {
734
799
  let payload;
735
800
  try {
@@ -813,10 +878,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
813
878
  }
814
879
  }
815
880
  // Drag & drop from OS file manager into the local pane (copy into current local folder)
816
- const osPaths = this.getDroppedOsPaths(ev);
817
- if (osPaths.length) {
881
+ if (captured.osPaths.length) {
818
882
  try {
819
- for (const p of osPaths) {
883
+ for (const p of captured.osPaths) {
820
884
  const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
821
885
  const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, baseName);
822
886
  if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(destPath)) {
@@ -836,8 +900,8 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
836
900
  }
837
901
  // Fallback: use Tabby's native drag parser, then write files to disk
838
902
  try {
839
- const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
840
- if (dirUpload) {
903
+ const dirUpload = captured.dirUploadPromise ? await captured.dirUploadPromise : null;
904
+ if (dirUpload && this.countDirectoryUploadItems(dirUpload) > 0) {
841
905
  await this.writeDirectoryUploadToLocal(this.localPath, dirUpload);
842
906
  await this.refreshLocal();
843
907
  return;
@@ -846,7 +910,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
846
910
  catch (e) {
847
911
  console.error('[SFTP-UI] startUploadFromDragEvent (local) failed', e);
848
912
  }
849
- const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
913
+ const raw = captured.internalPayload;
850
914
  if (!raw) {
851
915
  return;
852
916
  }
@@ -1017,11 +1081,85 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1017
1081
  }
1018
1082
  }
1019
1083
  }
1084
+ getElectronWebUtils() {
1085
+ try {
1086
+ const electron = __webpack_require__(/*! electron */ "electron");
1087
+ return electron?.webUtils ?? null;
1088
+ }
1089
+ catch {
1090
+ return null;
1091
+ }
1092
+ }
1093
+ countDirectoryUploadItems(dirUpload) {
1094
+ const childrens = dirUpload?.getChildrens?.() ?? [];
1095
+ let count = 0;
1096
+ for (const item of childrens) {
1097
+ if (typeof item?.getChildrens === 'function') {
1098
+ count += this.countDirectoryUploadItems(item);
1099
+ }
1100
+ else {
1101
+ count++;
1102
+ }
1103
+ }
1104
+ return count;
1105
+ }
1106
+ captureDropData(ev) {
1107
+ const dt = ev.dataTransfer;
1108
+ const types = dt ? Array.from(dt.types) : [];
1109
+ const files = dt?.files?.length ? Array.from(dt.files) : [];
1110
+ const uriList = dt?.getData('text/uri-list') || '';
1111
+ const textPlain = dt?.getData('text/plain') || '';
1112
+ const internalPayload = dt?.getData('application/x-tabby-sftp-ui') || '';
1113
+ const localMovePayload = dt?.getData('application/x-tabby-sftp-ui-local-move') || '';
1114
+ const osPaths = this.resolveDroppedOsPaths(files, uriList, textPlain);
1115
+ let dirUploadPromise = null;
1116
+ try {
1117
+ const promise = this.platform.startUploadFromDragEvent?.(ev, true);
1118
+ if (promise) {
1119
+ dirUploadPromise = promise;
1120
+ }
1121
+ }
1122
+ catch {
1123
+ // ignore
1124
+ }
1125
+ return { types, files, osPaths, internalPayload, localMovePayload, dirUploadPromise };
1126
+ }
1127
+ async uploadDroppedFilesToRemote(files, remoteDir) {
1128
+ if (!this.sftpSession) {
1129
+ return 0;
1130
+ }
1131
+ if (!files.length) {
1132
+ return 0;
1133
+ }
1134
+ let uploaded = 0;
1135
+ for (const file of files) {
1136
+ const upload = new tabby_core__WEBPACK_IMPORTED_MODULE_6__.HTMLFileUpload(file);
1137
+ const name = upload.getName();
1138
+ const existing = this.remoteEntries.find(e => e.name === name);
1139
+ if (existing) {
1140
+ const ok = await this.showReplaceConfirm(`Replace existing "${name}" on remote?`);
1141
+ if (!ok) {
1142
+ continue;
1143
+ }
1144
+ const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
1145
+ await this.deleteRemotePathRecursive(remoteTarget);
1146
+ }
1147
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
1148
+ this.trackTransfer(upload, 'upload', targetRemotePath, name);
1149
+ await this.sftpSession.upload(targetRemotePath, upload);
1150
+ uploaded++;
1151
+ }
1152
+ return uploaded;
1153
+ }
1020
1154
  getDroppedOsPaths(ev) {
1021
1155
  const dt = ev.dataTransfer;
1022
1156
  if (!dt) {
1023
1157
  return [];
1024
1158
  }
1159
+ const files = dt.files?.length ? Array.from(dt.files) : [];
1160
+ return this.resolveDroppedOsPaths(files, dt.getData('text/uri-list') || '', dt.getData('text/plain') || '');
1161
+ }
1162
+ resolveDroppedOsPaths(files, uriList, textPlain) {
1025
1163
  const isWin = os__WEBPACK_IMPORTED_MODULE_1__.platform() === 'win32';
1026
1164
  const isLocalPath = (p) => {
1027
1165
  if (isWin) {
@@ -1030,15 +1168,34 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1030
1168
  }
1031
1169
  return p.startsWith('/');
1032
1170
  };
1033
- // 1) Electron-style File.path
1034
- const filePaths = Array.from(dt.files ?? [])
1171
+ const dedupePaths = (paths) => {
1172
+ return [...new Set(paths)];
1173
+ };
1174
+ // 1) Electron-style File.path (legacy)
1175
+ const legacyFilePaths = dedupePaths(files
1035
1176
  .map(f => f.path)
1036
- .filter((p) => Boolean(p));
1037
- if (filePaths.length) {
1038
- return filePaths;
1177
+ .filter((p) => Boolean(p && isLocalPath(p))));
1178
+ if (legacyFilePaths.length) {
1179
+ return legacyFilePaths;
1180
+ }
1181
+ // 1b) Electron webUtils.getPathForFile (modern Electron / Tabby)
1182
+ const webUtils = this.getElectronWebUtils();
1183
+ if (webUtils && files.length) {
1184
+ const webUtilsPaths = dedupePaths(files
1185
+ .map(f => {
1186
+ try {
1187
+ return webUtils.getPathForFile(f);
1188
+ }
1189
+ catch {
1190
+ return '';
1191
+ }
1192
+ })
1193
+ .filter(p => p && isLocalPath(p)));
1194
+ if (webUtilsPaths.length) {
1195
+ return webUtilsPaths;
1196
+ }
1039
1197
  }
1040
1198
  // 2) Sometimes paths are exposed as URIs
1041
- const uriList = dt.getData('text/uri-list') || '';
1042
1199
  const uris = uriList
1043
1200
  .split(/\r?\n/g)
1044
1201
  .map(x => x.trim())
@@ -1066,8 +1223,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1066
1223
  return uris;
1067
1224
  }
1068
1225
  // 3) Plain text sometimes contains a local path
1069
- const text = dt.getData('text/plain') || '';
1070
- const textLines = text.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
1226
+ const textLines = textPlain.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
1071
1227
  const textPaths = textLines
1072
1228
  .map(x => {
1073
1229
  if (x.startsWith('file://')) {
@@ -1370,6 +1526,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1370
1526
  }
1371
1527
  }
1372
1528
  getRemoteBreadcrumbs() {
1529
+ if (this.isWindowsSftpPath(this.remotePath)) {
1530
+ return this.getWindowsSftpBreadcrumbs(this.remotePath);
1531
+ }
1373
1532
  const parts = this.remotePath.split('/').filter(Boolean);
1374
1533
  const crumbs = [];
1375
1534
  let current = '/';
@@ -1394,9 +1553,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1394
1553
  if (!this.connected) {
1395
1554
  return;
1396
1555
  }
1397
- const target = this.normalizeRemotePath(this.remotePathInput || '/');
1398
- this.remotePath = target;
1399
- this.remotePathInput = target;
1556
+ this.syncRemotePathFromInput();
1400
1557
  void this.refreshRemote();
1401
1558
  }
1402
1559
  goToLocalPathInput() {
@@ -1583,21 +1740,169 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1583
1740
  if (!p) {
1584
1741
  return '/';
1585
1742
  }
1586
- let result = p.trim();
1743
+ let result = p.trim().replace(/\\/g, '/');
1744
+ const winDrive = result.match(/^\/([A-Za-z]):(?:\/(.*))?$/);
1745
+ if (winDrive) {
1746
+ const rest = winDrive[2] ?? '';
1747
+ return rest
1748
+ ? `/${winDrive[1]}:/${rest}`.replace(/\/+/g, '/')
1749
+ : `/${winDrive[1]}:/`;
1750
+ }
1751
+ const bareDrive = result.match(/^([A-Za-z]):(?:\/(.*))?$/);
1752
+ if (bareDrive) {
1753
+ const rest = bareDrive[2] ?? '';
1754
+ return rest
1755
+ ? `/${bareDrive[1]}:/${rest}`.replace(/\/+/g, '/')
1756
+ : `/${bareDrive[1]}:/`;
1757
+ }
1587
1758
  if (!result.startsWith('/')) {
1588
1759
  result = '/' + result;
1589
1760
  }
1590
- // remove duplicate slashes
1591
- result = result.replace(/\/+/g, '/');
1592
- return result;
1761
+ return result.replace(/\/+/g, '/');
1593
1762
  }
1594
- getDefaultRemotePath() {
1595
- const username = (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
1596
- if (username) {
1597
- return username === 'root' ? `/root` : `/home/${username}`;
1763
+ async resolveDefaultRemotePath() {
1764
+ const canonical = await this.canonicalizeRemotePath('.');
1765
+ if (canonical) {
1766
+ const normalized = this.normalizeRemotePath(canonical);
1767
+ if (await this.remotePathExists(normalized)) {
1768
+ return normalized;
1769
+ }
1770
+ }
1771
+ if (this.shellSession) {
1772
+ try {
1773
+ const cwd = await this.shellSession.getWorkingDirectory?.();
1774
+ if (cwd) {
1775
+ const normalized = this.toRemoteSftpPath(cwd);
1776
+ if (await this.remotePathExists(normalized)) {
1777
+ return normalized;
1778
+ }
1779
+ }
1780
+ }
1781
+ catch {
1782
+ // fall through to SFTP-based detection
1783
+ }
1784
+ }
1785
+ const username = this.getRemoteUsername();
1786
+ for (const candidate of this.buildDefaultRemotePathCandidates(username)) {
1787
+ if (await this.remotePathExists(candidate)) {
1788
+ return candidate;
1789
+ }
1598
1790
  }
1599
1791
  return '/';
1600
1792
  }
1793
+ syncRemotePathFromInput() {
1794
+ const normalized = this.normalizeRemotePath(this.remotePathInput || this.remotePath || '/');
1795
+ this.remotePath = normalized;
1796
+ this.remotePathInput = normalized;
1797
+ }
1798
+ async ensureRemoteDirectoryReady() {
1799
+ if (!this.sftpSession) {
1800
+ return false;
1801
+ }
1802
+ this.syncRemotePathFromInput();
1803
+ if (await this.remotePathExists(this.remotePath)) {
1804
+ return true;
1805
+ }
1806
+ const resolved = await this.resolveDefaultRemotePath();
1807
+ this.remotePath = resolved;
1808
+ this.remotePathInput = resolved;
1809
+ return await this.remotePathExists(resolved);
1810
+ }
1811
+ getRemoteUsername() {
1812
+ return (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
1813
+ }
1814
+ buildDefaultRemotePathCandidates(username) {
1815
+ const candidates = [];
1816
+ if (username) {
1817
+ candidates.push(`/C:/Users/${username}`);
1818
+ if (username.toLowerCase() === 'administrator') {
1819
+ candidates.push('/C:/Users/Administrator');
1820
+ }
1821
+ if (username === 'root') {
1822
+ candidates.push('/root');
1823
+ }
1824
+ else {
1825
+ candidates.push(`/home/${username}`);
1826
+ }
1827
+ }
1828
+ candidates.push('/');
1829
+ return candidates;
1830
+ }
1831
+ async canonicalizeRemotePath(p) {
1832
+ const session = this.sftpSession;
1833
+ const candidates = [
1834
+ session?.canonicalize?.bind(session),
1835
+ session?.realpath?.bind(session),
1836
+ session?.sftp?.canonicalize?.bind(session.sftp),
1837
+ session?.sftp?.realpath?.bind(session.sftp),
1838
+ ].filter((fn) => typeof fn === 'function');
1839
+ for (const fn of candidates) {
1840
+ try {
1841
+ return await fn(p);
1842
+ }
1843
+ catch {
1844
+ // try the next available SFTP realpath implementation
1845
+ }
1846
+ }
1847
+ return null;
1848
+ }
1849
+ async remotePathExists(p) {
1850
+ if (!this.sftpSession) {
1851
+ return false;
1852
+ }
1853
+ try {
1854
+ if (this.sftpSession.stat) {
1855
+ const st = await this.sftpSession.stat(p);
1856
+ return st.isDirectory;
1857
+ }
1858
+ await this.sftpSession.readdir(p);
1859
+ return true;
1860
+ }
1861
+ catch {
1862
+ return false;
1863
+ }
1864
+ }
1865
+ toRemoteSftpPath(p) {
1866
+ return this.normalizeRemotePath(p);
1867
+ }
1868
+ isWindowsSftpPath(p) {
1869
+ return /^\/[A-Za-z]:\//.test(p) || /^\/[A-Za-z]:\/?$/.test(p);
1870
+ }
1871
+ getWindowsSftpBreadcrumbs(remotePath) {
1872
+ const match = remotePath.match(/^\/([A-Za-z]):\/?(.*)$/);
1873
+ if (!match) {
1874
+ return [{ label: '/', path: '/' }];
1875
+ }
1876
+ const drive = match[1].toUpperCase();
1877
+ const crumbs = [
1878
+ { label: `${drive}:`, path: `/${drive}:/` },
1879
+ ];
1880
+ let current = `/${drive}:/`;
1881
+ for (const part of match[2].split('/').filter(Boolean)) {
1882
+ current = `${current}${part}/`;
1883
+ crumbs.push({ label: part, path: current.replace(/\/+$/, '') || `/${drive}:/` });
1884
+ }
1885
+ return crumbs;
1886
+ }
1887
+ remoteParentPath(p) {
1888
+ if (this.isWindowsSftpPath(p)) {
1889
+ const match = p.match(/^\/([A-Za-z]):\/?(.*)$/);
1890
+ if (!match) {
1891
+ return '/';
1892
+ }
1893
+ const drive = match[1].toUpperCase();
1894
+ const parts = match[2].split('/').filter(Boolean);
1895
+ if (!parts.length) {
1896
+ return '/';
1897
+ }
1898
+ parts.pop();
1899
+ return parts.length
1900
+ ? `/${drive}:/${parts.join('/')}`
1901
+ : `/${drive}:/`;
1902
+ }
1903
+ const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(p);
1904
+ return next === '.' ? '/' : next;
1905
+ }
1601
1906
  loadRecentProfiles() {
1602
1907
  try {
1603
1908
  const rec = this.profilesService.getRecentProfiles?.();
@@ -2494,7 +2799,7 @@ SftpManagerTabComponent = __decorate([
2494
2799
  />
2495
2800
  </div>
2496
2801
  <div class="pane-actions">
2497
- <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2802
+ <button (click)="remoteUp()" [disabled]="!connected || !canRemoteUp()">Up</button>
2498
2803
  <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2499
2804
  <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2500
2805
  </div>
@@ -2532,7 +2837,7 @@ SftpManagerTabComponent = __decorate([
2532
2837
  </div>
2533
2838
  <div
2534
2839
  class="entry"
2535
- *ngIf="connected && remotePath !== '/'"
2840
+ *ngIf="connected && canRemoteUp()"
2536
2841
  (dblclick)="remoteUp()"
2537
2842
  >
2538
2843
  <span class="icon">⬆</span>
@@ -2969,6 +3274,7 @@ let SftpUiService = class SftpUiService {
2969
3274
  inputs: {
2970
3275
  sshSession,
2971
3276
  profile,
3277
+ shellSession: sourceTab?.session ?? null,
2972
3278
  },
2973
3279
  });
2974
3280
  tab.setTitle(`${baseTitle} + SFTP`);
@@ -3086,6 +3392,18 @@ module.exports = __WEBPACK_EXTERNAL_MODULE__angular_forms__;
3086
3392
 
3087
3393
  /***/ },
3088
3394
 
3395
+ /***/ "electron"
3396
+ /*!***************************!*\
3397
+ !*** external "electron" ***!
3398
+ \***************************/
3399
+ (module) {
3400
+
3401
+ if(typeof __WEBPACK_EXTERNAL_MODULE_electron__ === 'undefined') { var e = new Error("Cannot find module 'electron'"); e.code = 'MODULE_NOT_FOUND'; throw e; }
3402
+
3403
+ module.exports = __WEBPACK_EXTERNAL_MODULE_electron__;
3404
+
3405
+ /***/ },
3406
+
3089
3407
  /***/ "fs"
3090
3408
  /*!*********************!*\
3091
3409
  !*** external "fs" ***!