tabby-sftp-ui 0.2.4 → 0.2.6

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
@@ -237,6 +237,7 @@ __webpack_require__.r(__webpack_exports__);
237
237
  /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_6__);
238
238
  /* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
239
239
  /* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
240
+ /* harmony import */ var _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./sftp-recovery-provider */ "./src/sftp-recovery-provider.ts");
240
241
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
241
242
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
242
243
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -255,6 +256,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
255
256
 
256
257
 
257
258
 
259
+
258
260
  let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_6__.BaseTabComponent {
259
261
  constructor(injector, sftp, profilesService, app) {
260
262
  // Tabby runtime BaseTabComponent expects Injector in constructor, but typings in this SDK may differ.
@@ -265,7 +267,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
265
267
  this.app = app;
266
268
  // injected from the SSH tab when opened via SFTP-UI button
267
269
  this.sshSession = null;
270
+ this.shellSession = null;
268
271
  this.profile = null;
272
+ this.recoveredStub = false;
269
273
  this.connecting = false;
270
274
  this.connected = false;
271
275
  // legacy UI fields kept for now (not used when opened from SSH tab)
@@ -278,6 +282,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
278
282
  this.remotePath = '/';
279
283
  this.remoteEntries = [];
280
284
  this.sftpSession = null;
285
+ this.connectPromise = null;
281
286
  this.localDropActive = false;
282
287
  this.remoteDropActive = false;
283
288
  this.transfers = [];
@@ -353,9 +358,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
353
358
  // If there's no live SSH session, this tab was likely restored across
354
359
  // restart or opened in an invalid context. Close it immediately to avoid
355
360
  // an empty, nameless SFTP tab lingering after restart.
356
- if (!this.sshSession) {
361
+ if (this.recoveredStub || !this.sshSession) {
357
362
  try {
358
- this.app.closeTab(this);
363
+ this.app.closeTab(this.topmostParent ?? this);
359
364
  }
360
365
  catch (e) {
361
366
  console.error('[SFTP-UI] Failed to close invalid SFTP tab', e);
@@ -365,11 +370,32 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
365
370
  this.remotePathInput = this.remotePath;
366
371
  this.localPathInput = this.localPath;
367
372
  if (this.sshSession) {
368
- void this.connect();
373
+ void this.waitForConnection();
369
374
  }
370
375
  this.loadRecentProfiles();
371
376
  }
372
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() {
373
399
  if (this.connecting || this.connected) {
374
400
  return;
375
401
  }
@@ -381,17 +407,30 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
381
407
  try {
382
408
  this.sftpSession = await this.sftp.openFromSSHSession(this.sshSession);
383
409
  this.connected = true;
384
- this.remotePath = this.getDefaultRemotePath();
385
- this.remotePathInput = this.remotePath;
410
+ await this.initializeRemotePath();
386
411
  await this.refreshRemote();
387
412
  }
388
413
  catch (e) {
389
414
  console.error('[SFTP-UI] SFTP connection failed', e);
415
+ this.connected = false;
416
+ this.sftpSession = null;
390
417
  }
391
418
  finally {
392
419
  this.connecting = false;
393
420
  }
394
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
+ }
395
434
  async disconnect() {
396
435
  this.sftpSession = null;
397
436
  this.connected = false;
@@ -410,14 +449,26 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
410
449
  }
411
450
  }
412
451
  remoteUp() {
413
- if (!this.connected || this.remotePath === '/') {
452
+ if (!this.canRemoteUp()) {
414
453
  return;
415
454
  }
416
- const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(this.remotePath);
417
- this.remotePath = next === '.' ? '/' : next;
455
+ const next = this.remoteParentPath(this.remotePath);
456
+ this.remotePath = next;
418
457
  this.remotePathInput = this.remotePath;
419
458
  void this.refreshRemote();
420
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
+ }
421
472
  async refreshLocal() {
422
473
  try {
423
474
  const names = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(this.localPath);
@@ -630,10 +681,12 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
630
681
  async onDropOnRemote(ev) {
631
682
  ev.preventDefault();
632
683
  this.remoteDropActive = false;
633
- if (!this.connected) {
684
+ await this.waitForConnection();
685
+ if (!this.connected || !this.sftpSession) {
634
686
  return;
635
687
  }
636
- if (!this.sftpSession) {
688
+ if (!(await this.ensureRemoteDirectoryReady())) {
689
+ console.error('[SFTP-UI] Remote path is not ready for upload');
637
690
  return;
638
691
  }
639
692
  // Drag & drop from OS file manager (Explorer/Finder) into the remote pane
@@ -1367,6 +1420,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1367
1420
  }
1368
1421
  }
1369
1422
  getRemoteBreadcrumbs() {
1423
+ if (this.isWindowsSftpPath(this.remotePath)) {
1424
+ return this.getWindowsSftpBreadcrumbs(this.remotePath);
1425
+ }
1370
1426
  const parts = this.remotePath.split('/').filter(Boolean);
1371
1427
  const crumbs = [];
1372
1428
  let current = '/';
@@ -1391,9 +1447,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1391
1447
  if (!this.connected) {
1392
1448
  return;
1393
1449
  }
1394
- const target = this.normalizeRemotePath(this.remotePathInput || '/');
1395
- this.remotePath = target;
1396
- this.remotePathInput = target;
1450
+ this.syncRemotePathFromInput();
1397
1451
  void this.refreshRemote();
1398
1452
  }
1399
1453
  goToLocalPathInput() {
@@ -1580,21 +1634,169 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1580
1634
  if (!p) {
1581
1635
  return '/';
1582
1636
  }
1583
- let result = p.trim();
1637
+ let result = p.trim().replace(/\\/g, '/');
1638
+ const winDrive = result.match(/^\/([A-Za-z]):(?:\/(.*))?$/);
1639
+ if (winDrive) {
1640
+ const rest = winDrive[2] ?? '';
1641
+ return rest
1642
+ ? `/${winDrive[1]}:/${rest}`.replace(/\/+/g, '/')
1643
+ : `/${winDrive[1]}:/`;
1644
+ }
1645
+ const bareDrive = result.match(/^([A-Za-z]):(?:\/(.*))?$/);
1646
+ if (bareDrive) {
1647
+ const rest = bareDrive[2] ?? '';
1648
+ return rest
1649
+ ? `/${bareDrive[1]}:/${rest}`.replace(/\/+/g, '/')
1650
+ : `/${bareDrive[1]}:/`;
1651
+ }
1584
1652
  if (!result.startsWith('/')) {
1585
1653
  result = '/' + result;
1586
1654
  }
1587
- // remove duplicate slashes
1588
- result = result.replace(/\/+/g, '/');
1589
- return result;
1655
+ return result.replace(/\/+/g, '/');
1590
1656
  }
1591
- getDefaultRemotePath() {
1592
- const username = (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
1593
- if (username) {
1594
- return `/home/${username}`;
1657
+ async resolveDefaultRemotePath() {
1658
+ const canonical = await this.canonicalizeRemotePath('.');
1659
+ if (canonical) {
1660
+ const normalized = this.normalizeRemotePath(canonical);
1661
+ if (await this.remotePathExists(normalized)) {
1662
+ return normalized;
1663
+ }
1664
+ }
1665
+ if (this.shellSession) {
1666
+ try {
1667
+ const cwd = await this.shellSession.getWorkingDirectory?.();
1668
+ if (cwd) {
1669
+ const normalized = this.toRemoteSftpPath(cwd);
1670
+ if (await this.remotePathExists(normalized)) {
1671
+ return normalized;
1672
+ }
1673
+ }
1674
+ }
1675
+ catch {
1676
+ // fall through to SFTP-based detection
1677
+ }
1678
+ }
1679
+ const username = this.getRemoteUsername();
1680
+ for (const candidate of this.buildDefaultRemotePathCandidates(username)) {
1681
+ if (await this.remotePathExists(candidate)) {
1682
+ return candidate;
1683
+ }
1595
1684
  }
1596
1685
  return '/';
1597
1686
  }
1687
+ syncRemotePathFromInput() {
1688
+ const normalized = this.normalizeRemotePath(this.remotePathInput || this.remotePath || '/');
1689
+ this.remotePath = normalized;
1690
+ this.remotePathInput = normalized;
1691
+ }
1692
+ async ensureRemoteDirectoryReady() {
1693
+ if (!this.sftpSession) {
1694
+ return false;
1695
+ }
1696
+ this.syncRemotePathFromInput();
1697
+ if (await this.remotePathExists(this.remotePath)) {
1698
+ return true;
1699
+ }
1700
+ const resolved = await this.resolveDefaultRemotePath();
1701
+ this.remotePath = resolved;
1702
+ this.remotePathInput = resolved;
1703
+ return await this.remotePathExists(resolved);
1704
+ }
1705
+ getRemoteUsername() {
1706
+ return (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
1707
+ }
1708
+ buildDefaultRemotePathCandidates(username) {
1709
+ const candidates = [];
1710
+ if (username) {
1711
+ candidates.push(`/C:/Users/${username}`);
1712
+ if (username.toLowerCase() === 'administrator') {
1713
+ candidates.push('/C:/Users/Administrator');
1714
+ }
1715
+ if (username === 'root') {
1716
+ candidates.push('/root');
1717
+ }
1718
+ else {
1719
+ candidates.push(`/home/${username}`);
1720
+ }
1721
+ }
1722
+ candidates.push('/');
1723
+ return candidates;
1724
+ }
1725
+ async canonicalizeRemotePath(p) {
1726
+ const session = this.sftpSession;
1727
+ const candidates = [
1728
+ session?.canonicalize?.bind(session),
1729
+ session?.realpath?.bind(session),
1730
+ session?.sftp?.canonicalize?.bind(session.sftp),
1731
+ session?.sftp?.realpath?.bind(session.sftp),
1732
+ ].filter((fn) => typeof fn === 'function');
1733
+ for (const fn of candidates) {
1734
+ try {
1735
+ return await fn(p);
1736
+ }
1737
+ catch {
1738
+ // try the next available SFTP realpath implementation
1739
+ }
1740
+ }
1741
+ return null;
1742
+ }
1743
+ async remotePathExists(p) {
1744
+ if (!this.sftpSession) {
1745
+ return false;
1746
+ }
1747
+ try {
1748
+ if (this.sftpSession.stat) {
1749
+ const st = await this.sftpSession.stat(p);
1750
+ return st.isDirectory;
1751
+ }
1752
+ await this.sftpSession.readdir(p);
1753
+ return true;
1754
+ }
1755
+ catch {
1756
+ return false;
1757
+ }
1758
+ }
1759
+ toRemoteSftpPath(p) {
1760
+ return this.normalizeRemotePath(p);
1761
+ }
1762
+ isWindowsSftpPath(p) {
1763
+ return /^\/[A-Za-z]:\//.test(p) || /^\/[A-Za-z]:\/?$/.test(p);
1764
+ }
1765
+ getWindowsSftpBreadcrumbs(remotePath) {
1766
+ const match = remotePath.match(/^\/([A-Za-z]):\/?(.*)$/);
1767
+ if (!match) {
1768
+ return [{ label: '/', path: '/' }];
1769
+ }
1770
+ const drive = match[1].toUpperCase();
1771
+ const crumbs = [
1772
+ { label: `${drive}:`, path: `/${drive}:/` },
1773
+ ];
1774
+ let current = `/${drive}:/`;
1775
+ for (const part of match[2].split('/').filter(Boolean)) {
1776
+ current = `${current}${part}/`;
1777
+ crumbs.push({ label: part, path: current.replace(/\/+$/, '') || `/${drive}:/` });
1778
+ }
1779
+ return crumbs;
1780
+ }
1781
+ remoteParentPath(p) {
1782
+ if (this.isWindowsSftpPath(p)) {
1783
+ const match = p.match(/^\/([A-Za-z]):\/?(.*)$/);
1784
+ if (!match) {
1785
+ return '/';
1786
+ }
1787
+ const drive = match[1].toUpperCase();
1788
+ const parts = match[2].split('/').filter(Boolean);
1789
+ if (!parts.length) {
1790
+ return '/';
1791
+ }
1792
+ parts.pop();
1793
+ return parts.length
1794
+ ? `/${drive}:/${parts.join('/')}`
1795
+ : `/${drive}:/`;
1796
+ }
1797
+ const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(p);
1798
+ return next === '.' ? '/' : next;
1799
+ }
1598
1800
  loadRecentProfiles() {
1599
1801
  try {
1600
1802
  const rec = this.profilesService.getRecentProfiles?.();
@@ -2036,12 +2238,16 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
2036
2238
  }
2037
2239
  super.destroy();
2038
2240
  }
2039
- // Prevent Tabby from restoring SFTP-UI tabs across restarts, since they rely
2040
- // on a live SSH session from a terminal tab.
2041
- // Typинги допускают RecoveryToken | null, нам достаточно всегда возвращать null.
2241
+ // A custom recovery token ensures that a SplitTab wrapper won't become empty
2242
+ // after restart. On recovery we restore a stub SFTP tab which immediately
2243
+ // closes its `topmostParent` (the SplitTab wrapper).
2042
2244
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2043
2245
  async getRecoveryToken(_options) {
2044
- return null;
2246
+ return {
2247
+ type: _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_9__.SFTP_RECOVERY_TYPE,
2248
+ version: 1,
2249
+ state: {},
2250
+ };
2045
2251
  }
2046
2252
  async confirmDelete() {
2047
2253
  if (!this.deleteConfirmVisible) {
@@ -2339,388 +2545,388 @@ __decorate([
2339
2545
  SftpManagerTabComponent = __decorate([
2340
2546
  (0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
2341
2547
  selector: 'tabby-sftp-manager-tab',
2342
- template: `
2343
- <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
2344
- <div class="top-profiles" *ngIf="profile || recentProfiles.length">
2345
- <div class="current" *ngIf="profile">
2346
- <span class="label">Device:</span>
2347
- <span class="value">{{ getProfileLabel(profile) }}</span>
2348
- </div>
2349
- <div class="recent" *ngIf="recentProfiles.length">
2350
- <span class="label">Recent:</span>
2351
- <button
2352
- class="profile-chip"
2353
- *ngFor="let p of recentProfiles"
2354
- (click)="launchProfileFromSFTP(p)"
2355
- >
2356
- {{ getProfileLabel(p) }}
2357
- </button>
2358
- </div>
2359
- </div>
2360
- <div class="sftp-body">
2361
- <div class="pane">
2362
- <div class="pane-title">
2363
- <div class="pane-label">Local</div>
2364
- <div class="pane-path">
2365
- <input
2366
- [(ngModel)]="localPathInput"
2367
- (keyup.enter)="goToLocalPathInput()"
2368
- />
2369
- </div>
2370
- <div class="pane-actions">
2371
- <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2372
- <option value="">Go to…</option>
2373
- <option *ngFor="let p of localPathPresets" [value]="p.id">
2374
- {{ p.label }}
2375
- </option>
2376
- </select>
2377
- <button
2378
- class="fav-toggle"
2379
- [class.active]="isCurrentFavorite()"
2380
- (click)="toggleCurrentFavorite()"
2381
- title="Toggle favorite for this path"
2382
- >
2383
-
2384
- </button>
2385
- <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2386
- <option value="">Favorites…</option>
2387
- <option *ngFor="let f of localFavorites" [value]="f.id">
2388
- {{ f.label }}
2389
- </option>
2390
- </select>
2391
- <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2392
- <button (click)="goToLocalPathInput()">Go</button>
2393
- <button (click)="refreshLocal()">Refresh</button>
2394
- </div>
2395
- </div>
2396
- <div class="pane-filters">
2397
- <div class="breadcrumbs">
2398
- <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2399
- <button
2400
- class="crumb-button"
2401
- (click)="navigateLocalBreadcrumb(i)"
2402
- (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2403
- >
2404
- {{ part.label }}
2405
- </button>
2406
- <span class="crumb-separator" *ngIf="!last">›</span>
2407
- </ng-container>
2408
- </div>
2409
- <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2410
- <label class="show-hidden-toggle">
2411
- <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2412
- <span>Show hidden</span>
2413
- </label>
2414
- </div>
2415
- <div class="pane-list"
2416
- (dragover)="onDragOver($event)"
2417
- (drop)="onDropOnLocal($event)"
2418
- >
2419
- <div class="entry header">
2420
- <span class="icon"></span>
2421
- <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2422
- <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2423
- <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2424
- </div>
2425
- <div
2426
- class="entry"
2427
- *ngIf="canLocalUp()"
2428
- (dblclick)="localUp()"
2429
- >
2430
- <span class="icon">⬆</span>
2431
- <span class="name">Go up</span>
2432
- <span class="size"></span>
2433
- <span class="date"></span>
2434
- </div>
2435
- <div
2436
- class="entry"
2437
- *ngFor="let e of getFilteredLocalEntries()"
2438
- (click)="selectLocal(e, $event)"
2439
- (dblclick)="openLocal(e)"
2440
- (mousedown)="onLocalMouseDown(e, $event)"
2441
- (contextmenu)="onLocalContextMenu(e, $event)"
2442
- (dragover)="onLocalEntryDragOver(e, $event)"
2443
- (drop)="onLocalEntryDrop(e, $event)"
2444
- [class.drop-target]="localDropActive"
2445
- [class.selected]="isLocalSelected(e)"
2446
- [draggable]="true"
2447
- (dragstart)="onDragStartLocal($event, e)"
2448
- >
2449
- <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2450
- <span class="name">{{ e.name }}</span>
2451
- <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2452
- <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2453
- </div>
2454
- </div>
2455
- <div class="pane-actions-bar">
2456
- <div class="selection" *ngIf="selectedLocal.length">
2457
- Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2458
- </div>
2459
- <div class="action-inputs">
2460
- <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2461
- <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2462
- </div>
2463
- <div class="action-buttons">
2464
- <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2465
- <button (click)="refreshLocal()">Refresh</button>
2466
- <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2467
- <button (click)="localNewFolder()">New Folder</button>
2468
- <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2469
- <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2470
- </div>
2471
- </div>
2472
- </div>
2473
-
2474
- <div class="pane">
2475
- <div class="pane-title">
2476
- <div class="pane-label">
2477
- Remote
2478
- <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2479
- — {{ profile.options.host }}
2480
- </span>
2481
- </div>
2482
- <div class="pane-path">
2483
- <input
2484
- [(ngModel)]="remotePathInput"
2485
- (keyup.enter)="goToRemotePathInput()"
2486
- [disabled]="!connected"
2487
- />
2488
- </div>
2489
- <div class="pane-actions">
2490
- <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2491
- <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2492
- <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2493
- </div>
2494
- </div>
2495
- <div class="pane-filters">
2496
- <div class="breadcrumbs" *ngIf="connected">
2497
- <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2498
- <button
2499
- class="crumb-button"
2500
- (click)="navigateRemoteBreadcrumb(i)"
2501
- >
2502
- {{ part.label }}
2503
- </button>
2504
- <span class="crumb-separator" *ngIf="!last">›</span>
2505
- </ng-container>
2506
- </div>
2507
- <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2508
- <label class="show-hidden-toggle">
2509
- <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2510
- <span>Show hidden</span>
2511
- </label>
2512
- </div>
2513
- <div class="pane-list"
2514
- (dragover)="onDragOver($event)"
2515
- (drop)="onDropOnRemote($event)"
2516
- >
2517
- <div class="entry dim" *ngIf="!connected">
2518
- <span class="name">Not connected</span>
2519
- </div>
2520
- <div class="entry header" *ngIf="connected">
2521
- <span class="icon"></span>
2522
- <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2523
- <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2524
- <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2525
- </div>
2526
- <div
2527
- class="entry"
2528
- *ngIf="connected && remotePath !== '/'"
2529
- (dblclick)="remoteUp()"
2530
- >
2531
- <span class="icon">⬆</span>
2532
- <span class="name">Go up</span>
2533
- <span class="size"></span>
2534
- <span class="date"></span>
2535
- </div>
2536
- <div
2537
- class="entry"
2538
- *ngFor="let e of getFilteredRemoteEntries()"
2539
- (click)="selectRemote(e, $event)"
2540
- (dblclick)="openRemote(e)"
2541
- (mousedown)="onRemoteMouseDown(e, $event)"
2542
- (contextmenu)="onRemoteContextMenu(e, $event)"
2543
- (dragover)="onRemoteEntryDragOver(e, $event)"
2544
- (drop)="onRemoteEntryDrop(e, $event)"
2545
- [class.drop-target]="remoteDropActive"
2546
- [class.selected]="isRemoteSelected(e)"
2547
- [draggable]="connected"
2548
- (dragstart)="onDragStartRemote($event, e)"
2549
- >
2550
- <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2551
- <span class="name">{{ e.name }}</span>
2552
- <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2553
- <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2554
- </div>
2555
- </div>
2556
- <div class="pane-actions-bar">
2557
- <div class="selection" *ngIf="selectedRemote.length">
2558
- Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2559
- </div>
2560
- <div class="action-inputs">
2561
- <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2562
- <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2563
- </div>
2564
- <div class="action-buttons">
2565
- <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2566
- <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2567
- <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2568
- <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2569
- <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2570
- <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2571
- <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2572
- </div>
2573
- </div>
2574
- </div>
2575
- </div>
2576
- <div class="sftp-transfers" *ngIf="transfers.length">
2577
- <div class="transfer" *ngFor="let t of transfers">
2578
- <div class="transfer-main">
2579
- <div class="transfer-title">
2580
- <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2581
- <span class="name">{{ t.name }}</span>
2582
- </div>
2583
- <div class="transfer-path">
2584
- <span class="label">Remote:</span>
2585
- <span class="value">{{ t.remotePath }}</span>
2586
- </div>
2587
- <div class="transfer-path">
2588
- <span class="label">Local:</span>
2589
- <span class="value">{{ t.localPath }}</span>
2590
- </div>
2591
- <div class="bar">
2592
- <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2593
- </div>
2594
- </div>
2595
- <div class="transfer-stats">
2596
- <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2597
- <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2598
- <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2599
- </div>
2600
- </div>
2601
- </div>
2602
-
2603
- <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2604
- <div class="delete-dialog">
2605
- <div class="delete-text">{{ deleteConfirmText }}</div>
2606
- <div class="delete-buttons">
2607
- <button class="danger" (click)="confirmDelete()">Delete</button>
2608
- <button (click)="cancelDelete()">Cancel</button>
2609
- </div>
2610
- </div>
2611
- </div>
2612
-
2613
- <div class="delete-overlay" *ngIf="replaceConfirmVisible">
2614
- <div class="delete-dialog">
2615
- <div class="delete-text">{{ replaceConfirmText }}</div>
2616
- <div class="delete-buttons">
2617
- <button class="danger" (click)="confirmReplace()">Replace</button>
2618
- <button (click)="cancelReplace()">Cancel</button>
2619
- </div>
2620
- </div>
2621
- </div>
2622
-
2623
- <div class="delete-overlay" *ngIf="inputDialogVisible">
2624
- <div class="delete-dialog" (click)="$event.stopPropagation()">
2625
- <div class="delete-text">{{ inputDialogTitle }}</div>
2626
- <input
2627
- class="dialog-input"
2628
- [(ngModel)]="inputDialogValue"
2629
- [placeholder]="inputDialogPlaceholder"
2630
- (keyup.enter)="confirmInputDialog()"
2631
- />
2632
- <div class="delete-buttons">
2633
- <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2634
- <button (click)="cancelInputDialog()">Cancel</button>
2635
- </div>
2636
- </div>
2637
- </div>
2638
-
2639
- <div
2640
- class="local-menu"
2641
- *ngIf="localMenuVisible"
2642
- [style.left.px]="localMenuX"
2643
- [style.top.px]="localMenuY"
2644
- (click)="$event.stopPropagation()"
2645
- >
2646
- <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2647
- {{ item.label }}
2648
- </div>
2649
- </div>
2650
- </div>
2548
+ template: `
2549
+ <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
2550
+ <div class="top-profiles" *ngIf="profile || recentProfiles.length">
2551
+ <div class="current" *ngIf="profile">
2552
+ <span class="label">Device:</span>
2553
+ <span class="value">{{ getProfileLabel(profile) }}</span>
2554
+ </div>
2555
+ <div class="recent" *ngIf="recentProfiles.length">
2556
+ <span class="label">Recent:</span>
2557
+ <button
2558
+ class="profile-chip"
2559
+ *ngFor="let p of recentProfiles"
2560
+ (click)="launchProfileFromSFTP(p)"
2561
+ >
2562
+ {{ getProfileLabel(p) }}
2563
+ </button>
2564
+ </div>
2565
+ </div>
2566
+ <div class="sftp-body">
2567
+ <div class="pane">
2568
+ <div class="pane-title">
2569
+ <div class="pane-label">Local</div>
2570
+ <div class="pane-path">
2571
+ <input
2572
+ [(ngModel)]="localPathInput"
2573
+ (keyup.enter)="goToLocalPathInput()"
2574
+ />
2575
+ </div>
2576
+ <div class="pane-actions">
2577
+ <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2578
+ <option value="">Go to…</option>
2579
+ <option *ngFor="let p of localPathPresets" [value]="p.id">
2580
+ {{ p.label }}
2581
+ </option>
2582
+ </select>
2583
+ <button
2584
+ class="fav-toggle"
2585
+ [class.active]="isCurrentFavorite()"
2586
+ (click)="toggleCurrentFavorite()"
2587
+ title="Toggle favorite for this path"
2588
+ >
2589
+
2590
+ </button>
2591
+ <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2592
+ <option value="">Favorites…</option>
2593
+ <option *ngFor="let f of localFavorites" [value]="f.id">
2594
+ {{ f.label }}
2595
+ </option>
2596
+ </select>
2597
+ <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2598
+ <button (click)="goToLocalPathInput()">Go</button>
2599
+ <button (click)="refreshLocal()">Refresh</button>
2600
+ </div>
2601
+ </div>
2602
+ <div class="pane-filters">
2603
+ <div class="breadcrumbs">
2604
+ <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2605
+ <button
2606
+ class="crumb-button"
2607
+ (click)="navigateLocalBreadcrumb(i)"
2608
+ (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2609
+ >
2610
+ {{ part.label }}
2611
+ </button>
2612
+ <span class="crumb-separator" *ngIf="!last">›</span>
2613
+ </ng-container>
2614
+ </div>
2615
+ <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2616
+ <label class="show-hidden-toggle">
2617
+ <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2618
+ <span>Show hidden</span>
2619
+ </label>
2620
+ </div>
2621
+ <div class="pane-list"
2622
+ (dragover)="onDragOver($event)"
2623
+ (drop)="onDropOnLocal($event)"
2624
+ >
2625
+ <div class="entry header">
2626
+ <span class="icon"></span>
2627
+ <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2628
+ <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2629
+ <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2630
+ </div>
2631
+ <div
2632
+ class="entry"
2633
+ *ngIf="canLocalUp()"
2634
+ (dblclick)="localUp()"
2635
+ >
2636
+ <span class="icon">⬆</span>
2637
+ <span class="name">Go up</span>
2638
+ <span class="size"></span>
2639
+ <span class="date"></span>
2640
+ </div>
2641
+ <div
2642
+ class="entry"
2643
+ *ngFor="let e of getFilteredLocalEntries()"
2644
+ (click)="selectLocal(e, $event)"
2645
+ (dblclick)="openLocal(e)"
2646
+ (mousedown)="onLocalMouseDown(e, $event)"
2647
+ (contextmenu)="onLocalContextMenu(e, $event)"
2648
+ (dragover)="onLocalEntryDragOver(e, $event)"
2649
+ (drop)="onLocalEntryDrop(e, $event)"
2650
+ [class.drop-target]="localDropActive"
2651
+ [class.selected]="isLocalSelected(e)"
2652
+ [draggable]="true"
2653
+ (dragstart)="onDragStartLocal($event, e)"
2654
+ >
2655
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2656
+ <span class="name">{{ e.name }}</span>
2657
+ <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2658
+ <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2659
+ </div>
2660
+ </div>
2661
+ <div class="pane-actions-bar">
2662
+ <div class="selection" *ngIf="selectedLocal.length">
2663
+ Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2664
+ </div>
2665
+ <div class="action-inputs">
2666
+ <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2667
+ <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2668
+ </div>
2669
+ <div class="action-buttons">
2670
+ <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2671
+ <button (click)="refreshLocal()">Refresh</button>
2672
+ <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2673
+ <button (click)="localNewFolder()">New Folder</button>
2674
+ <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2675
+ <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2676
+ </div>
2677
+ </div>
2678
+ </div>
2679
+
2680
+ <div class="pane">
2681
+ <div class="pane-title">
2682
+ <div class="pane-label">
2683
+ Remote
2684
+ <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2685
+ — {{ profile.options.host }}
2686
+ </span>
2687
+ </div>
2688
+ <div class="pane-path">
2689
+ <input
2690
+ [(ngModel)]="remotePathInput"
2691
+ (keyup.enter)="goToRemotePathInput()"
2692
+ [disabled]="!connected"
2693
+ />
2694
+ </div>
2695
+ <div class="pane-actions">
2696
+ <button (click)="remoteUp()" [disabled]="!connected || !canRemoteUp()">Up</button>
2697
+ <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2698
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2699
+ </div>
2700
+ </div>
2701
+ <div class="pane-filters">
2702
+ <div class="breadcrumbs" *ngIf="connected">
2703
+ <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2704
+ <button
2705
+ class="crumb-button"
2706
+ (click)="navigateRemoteBreadcrumb(i)"
2707
+ >
2708
+ {{ part.label }}
2709
+ </button>
2710
+ <span class="crumb-separator" *ngIf="!last">›</span>
2711
+ </ng-container>
2712
+ </div>
2713
+ <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2714
+ <label class="show-hidden-toggle">
2715
+ <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2716
+ <span>Show hidden</span>
2717
+ </label>
2718
+ </div>
2719
+ <div class="pane-list"
2720
+ (dragover)="onDragOver($event)"
2721
+ (drop)="onDropOnRemote($event)"
2722
+ >
2723
+ <div class="entry dim" *ngIf="!connected">
2724
+ <span class="name">Not connected</span>
2725
+ </div>
2726
+ <div class="entry header" *ngIf="connected">
2727
+ <span class="icon"></span>
2728
+ <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2729
+ <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2730
+ <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2731
+ </div>
2732
+ <div
2733
+ class="entry"
2734
+ *ngIf="connected && canRemoteUp()"
2735
+ (dblclick)="remoteUp()"
2736
+ >
2737
+ <span class="icon">⬆</span>
2738
+ <span class="name">Go up</span>
2739
+ <span class="size"></span>
2740
+ <span class="date"></span>
2741
+ </div>
2742
+ <div
2743
+ class="entry"
2744
+ *ngFor="let e of getFilteredRemoteEntries()"
2745
+ (click)="selectRemote(e, $event)"
2746
+ (dblclick)="openRemote(e)"
2747
+ (mousedown)="onRemoteMouseDown(e, $event)"
2748
+ (contextmenu)="onRemoteContextMenu(e, $event)"
2749
+ (dragover)="onRemoteEntryDragOver(e, $event)"
2750
+ (drop)="onRemoteEntryDrop(e, $event)"
2751
+ [class.drop-target]="remoteDropActive"
2752
+ [class.selected]="isRemoteSelected(e)"
2753
+ [draggable]="connected"
2754
+ (dragstart)="onDragStartRemote($event, e)"
2755
+ >
2756
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2757
+ <span class="name">{{ e.name }}</span>
2758
+ <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2759
+ <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2760
+ </div>
2761
+ </div>
2762
+ <div class="pane-actions-bar">
2763
+ <div class="selection" *ngIf="selectedRemote.length">
2764
+ Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2765
+ </div>
2766
+ <div class="action-inputs">
2767
+ <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2768
+ <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2769
+ </div>
2770
+ <div class="action-buttons">
2771
+ <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2772
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2773
+ <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2774
+ <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2775
+ <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2776
+ <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2777
+ <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2778
+ </div>
2779
+ </div>
2780
+ </div>
2781
+ </div>
2782
+ <div class="sftp-transfers" *ngIf="transfers.length">
2783
+ <div class="transfer" *ngFor="let t of transfers">
2784
+ <div class="transfer-main">
2785
+ <div class="transfer-title">
2786
+ <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2787
+ <span class="name">{{ t.name }}</span>
2788
+ </div>
2789
+ <div class="transfer-path">
2790
+ <span class="label">Remote:</span>
2791
+ <span class="value">{{ t.remotePath }}</span>
2792
+ </div>
2793
+ <div class="transfer-path">
2794
+ <span class="label">Local:</span>
2795
+ <span class="value">{{ t.localPath }}</span>
2796
+ </div>
2797
+ <div class="bar">
2798
+ <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2799
+ </div>
2800
+ </div>
2801
+ <div class="transfer-stats">
2802
+ <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2803
+ <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2804
+ <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2805
+ </div>
2806
+ </div>
2807
+ </div>
2808
+
2809
+ <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2810
+ <div class="delete-dialog">
2811
+ <div class="delete-text">{{ deleteConfirmText }}</div>
2812
+ <div class="delete-buttons">
2813
+ <button class="danger" (click)="confirmDelete()">Delete</button>
2814
+ <button (click)="cancelDelete()">Cancel</button>
2815
+ </div>
2816
+ </div>
2817
+ </div>
2818
+
2819
+ <div class="delete-overlay" *ngIf="replaceConfirmVisible">
2820
+ <div class="delete-dialog">
2821
+ <div class="delete-text">{{ replaceConfirmText }}</div>
2822
+ <div class="delete-buttons">
2823
+ <button class="danger" (click)="confirmReplace()">Replace</button>
2824
+ <button (click)="cancelReplace()">Cancel</button>
2825
+ </div>
2826
+ </div>
2827
+ </div>
2828
+
2829
+ <div class="delete-overlay" *ngIf="inputDialogVisible">
2830
+ <div class="delete-dialog" (click)="$event.stopPropagation()">
2831
+ <div class="delete-text">{{ inputDialogTitle }}</div>
2832
+ <input
2833
+ class="dialog-input"
2834
+ [(ngModel)]="inputDialogValue"
2835
+ [placeholder]="inputDialogPlaceholder"
2836
+ (keyup.enter)="confirmInputDialog()"
2837
+ />
2838
+ <div class="delete-buttons">
2839
+ <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2840
+ <button (click)="cancelInputDialog()">Cancel</button>
2841
+ </div>
2842
+ </div>
2843
+ </div>
2844
+
2845
+ <div
2846
+ class="local-menu"
2847
+ *ngIf="localMenuVisible"
2848
+ [style.left.px]="localMenuX"
2849
+ [style.top.px]="localMenuY"
2850
+ (click)="$event.stopPropagation()"
2851
+ >
2852
+ <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2853
+ {{ item.label }}
2854
+ </div>
2855
+ </div>
2856
+ </div>
2651
2857
  `,
2652
- styles: [`
2653
- .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2654
- button { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; }
2655
- button:disabled { opacity: 0.5; cursor: default; }
2656
- .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2657
- .top-profiles .current .label,
2658
- .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2659
- .top-profiles .value { font-weight: 600; }
2660
- .top-profiles .profile-chip { padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2661
- .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2662
- .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2663
- .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2664
- .pane-title { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); }
2665
- .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2666
- .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2667
- .pane-path { opacity: 0.8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2668
- .pane-path input { width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-family: inherit; font-size: 12px; }
2669
- .pane-actions { display: flex; gap: 8px; align-items: center; }
2670
- .pane-actions .path-preset,
2671
- .pane-actions .path-favorite { max-width: 150px; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.22); background: rgba(20,20,20,0.95); color: inherit; font-size: 11px; }
2672
- .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2673
- .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2674
- .pane-actions .fav-toggle { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); font-size: 11px; line-height: 1; }
2675
- .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2676
- .pane-filters { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.12); }
2677
- .pane-filters input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 12px; }
2678
- .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2679
- .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2680
- .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2681
- .crumb-button { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2682
- .crumb-button:hover { background: rgba(255,255,255,0.10); }
2683
- .crumb-separator { opacity: 0.6; }
2684
- .pane-list { flex: 1; overflow: auto; padding: 4px; }
2685
- .entry { display: grid; grid-template-columns: 24px minmax(0, 1.5fr) 80px 140px; gap: 8px; padding: 6px 8px; border-radius: 8px; user-select: none; align-items: center; }
2686
- .entry:hover { background: rgba(255,255,255,0.06); }
2687
- .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2688
- .entry.dim { opacity: 0.7; }
2689
- .icon { text-align: center; opacity: 0.85; }
2690
- .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2691
- .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2692
- .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2693
- .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2694
- .sortable { cursor: pointer; }
2695
- .entry.selected { background: rgba(80,160,255,0.18); }
2696
- .pane-actions-bar { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-top: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.18); }
2697
- .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2698
- .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2699
- .pane-actions-bar .action-inputs input { flex: 1; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 11px; }
2700
- .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2701
- .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2702
- .transfer { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); font-size: 11px; }
2703
- .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2704
- .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2705
- .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2706
- .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2707
- .transfer-path .label { min-width: 48px; }
2708
- .transfer-path .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2709
- .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2710
- .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2711
- .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2712
- .transfer-stats .percent { font-weight: 600; }
2713
- .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2714
- .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2715
- .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2716
- .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; }
2717
- .delete-text { font-size: 13px; }
2718
- .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; }
2719
- .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2720
- .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2721
- .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); }
2722
- .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2723
- .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2858
+ styles: [`
2859
+ .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2860
+ button { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; }
2861
+ button:disabled { opacity: 0.5; cursor: default; }
2862
+ .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2863
+ .top-profiles .current .label,
2864
+ .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2865
+ .top-profiles .value { font-weight: 600; }
2866
+ .top-profiles .profile-chip { padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2867
+ .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2868
+ .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2869
+ .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2870
+ .pane-title { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); }
2871
+ .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2872
+ .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2873
+ .pane-path { opacity: 0.8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2874
+ .pane-path input { width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-family: inherit; font-size: 12px; }
2875
+ .pane-actions { display: flex; gap: 8px; align-items: center; }
2876
+ .pane-actions .path-preset,
2877
+ .pane-actions .path-favorite { max-width: 150px; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.22); background: rgba(20,20,20,0.95); color: inherit; font-size: 11px; }
2878
+ .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2879
+ .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2880
+ .pane-actions .fav-toggle { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); font-size: 11px; line-height: 1; }
2881
+ .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2882
+ .pane-filters { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.12); }
2883
+ .pane-filters input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 12px; }
2884
+ .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2885
+ .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2886
+ .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2887
+ .crumb-button { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
2888
+ .crumb-button:hover { background: rgba(255,255,255,0.10); }
2889
+ .crumb-separator { opacity: 0.6; }
2890
+ .pane-list { flex: 1; overflow: auto; padding: 4px; }
2891
+ .entry { display: grid; grid-template-columns: 24px minmax(0, 1.5fr) 80px 140px; gap: 8px; padding: 6px 8px; border-radius: 8px; user-select: none; align-items: center; }
2892
+ .entry:hover { background: rgba(255,255,255,0.06); }
2893
+ .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2894
+ .entry.dim { opacity: 0.7; }
2895
+ .icon { text-align: center; opacity: 0.85; }
2896
+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2897
+ .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2898
+ .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2899
+ .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2900
+ .sortable { cursor: pointer; }
2901
+ .entry.selected { background: rgba(80,160,255,0.18); }
2902
+ .pane-actions-bar { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-top: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.18); }
2903
+ .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2904
+ .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2905
+ .pane-actions-bar .action-inputs input { flex: 1; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 11px; }
2906
+ .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2907
+ .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2908
+ .transfer { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); font-size: 11px; }
2909
+ .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2910
+ .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2911
+ .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2912
+ .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2913
+ .transfer-path .label { min-width: 48px; }
2914
+ .transfer-path .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2915
+ .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2916
+ .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2917
+ .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2918
+ .transfer-stats .percent { font-weight: 600; }
2919
+ .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2920
+ .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2921
+ .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2922
+ .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; }
2923
+ .delete-text { font-size: 13px; }
2924
+ .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; }
2925
+ .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2926
+ .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2927
+ .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); }
2928
+ .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2929
+ .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2724
2930
  `],
2725
2931
  }),
2726
2932
  __metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
@@ -2731,6 +2937,57 @@ SftpManagerTabComponent = __decorate([
2731
2937
 
2732
2938
 
2733
2939
 
2940
+ /***/ },
2941
+
2942
+ /***/ "./src/sftp-recovery-provider.ts"
2943
+ /*!***************************************!*\
2944
+ !*** ./src/sftp-recovery-provider.ts ***!
2945
+ \***************************************/
2946
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
2947
+
2948
+ __webpack_require__.r(__webpack_exports__);
2949
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2950
+ /* harmony export */ SFTP_RECOVERY_TYPE: () => (/* binding */ SFTP_RECOVERY_TYPE),
2951
+ /* harmony export */ SftpTabRecoveryProvider: () => (/* binding */ SftpTabRecoveryProvider)
2952
+ /* harmony export */ });
2953
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
2954
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
2955
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
2956
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
2957
+ /* harmony import */ var _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-manager-tab.component */ "./src/sftp-manager-tab.component.ts");
2958
+ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
2959
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2960
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2961
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2962
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2963
+ };
2964
+
2965
+
2966
+
2967
+ const SFTP_RECOVERY_TYPE = 'tabby-sftp-ui:sftp-tab';
2968
+ let SftpTabRecoveryProvider = class SftpTabRecoveryProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.TabRecoveryProvider {
2969
+ async applicableTo(recoveryToken) {
2970
+ return recoveryToken?.type === SFTP_RECOVERY_TYPE;
2971
+ }
2972
+ async recover(_recoveryToken) {
2973
+ // Restore a stub tab that immediately closes itself and its SplitTab wrapper.
2974
+ return {
2975
+ type: _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__.SftpManagerTabComponent,
2976
+ inputs: {
2977
+ sshSession: null,
2978
+ profile: null,
2979
+ recoveredStub: true,
2980
+ },
2981
+ };
2982
+ }
2983
+ };
2984
+ SftpTabRecoveryProvider = __decorate([
2985
+ (0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)()
2986
+ ], SftpTabRecoveryProvider);
2987
+
2988
+
2989
+
2990
+
2734
2991
  /***/ },
2735
2992
 
2736
2993
  /***/ "./src/sftp-terminal-decorator.ts"
@@ -2768,7 +3025,13 @@ let SftpTerminalDecorator = class SftpTerminalDecorator extends tabby_terminal__
2768
3025
  attach(terminal) {
2769
3026
  super.attach(terminal);
2770
3027
  // Best-effort DOM injection: place button near the existing Reconnect button if present.
3028
+ // Only inject for SSH tabs (with a live sshSession property).
2771
3029
  const tryInsert = () => {
3030
+ // Skip for local terminals (PowerShell, CMD, etc.) - only SSH tabs need this button.
3031
+ const sshSession = terminal.sshSession;
3032
+ if (!sshSession) {
3033
+ return false;
3034
+ }
2772
3035
  try {
2773
3036
  const host = terminal.element?.nativeElement ?? null;
2774
3037
  if (!host) {
@@ -2905,6 +3168,7 @@ let SftpUiService = class SftpUiService {
2905
3168
  inputs: {
2906
3169
  sshSession,
2907
3170
  profile,
3171
+ shellSession: sourceTab?.session ?? null,
2908
3172
  },
2909
3173
  });
2910
3174
  tab.setTitle(`${baseTitle} + SFTP`);
@@ -3162,6 +3426,7 @@ __webpack_require__.r(__webpack_exports__);
3162
3426
  /* harmony import */ var _sftp_hotkey__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./sftp-hotkey */ "./src/sftp-hotkey.ts");
3163
3427
  /* harmony import */ var _sftp_ui_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp-ui.service */ "./src/sftp-ui.service.ts");
3164
3428
  /* harmony import */ var _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./sftp-terminal-decorator */ "./src/sftp-terminal-decorator.ts");
3429
+ /* harmony import */ var _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./sftp-recovery-provider */ "./src/sftp-recovery-provider.ts");
3165
3430
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
3166
3431
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3167
3432
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3181,6 +3446,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
3181
3446
 
3182
3447
 
3183
3448
 
3449
+
3184
3450
  let SftpUiModule = class SftpUiModule {
3185
3451
  constructor(_) { }
3186
3452
  };
@@ -3197,6 +3463,7 @@ SftpUiModule = __decorate([
3197
3463
  { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabContextMenuItemProvider, useClass: _sftp_context_menu__WEBPACK_IMPORTED_MODULE_6__.SftpContextMenuProvider, multi: true },
3198
3464
  { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.HotkeyProvider, useClass: _sftp_hotkey__WEBPACK_IMPORTED_MODULE_7__.SftpUiHotkeyProvider, multi: true },
3199
3465
  { provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_4__.TerminalDecorator, useClass: _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_9__.SftpTerminalDecorator, multi: true },
3466
+ { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabRecoveryProvider, useClass: _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_10__.SftpTabRecoveryProvider, multi: true },
3200
3467
  _sftp_ui_service__WEBPACK_IMPORTED_MODULE_8__.SftpUiService,
3201
3468
  ],
3202
3469
  }),