tabby-sftp-ui 0.2.4 → 0.2.5

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.
@@ -266,6 +268,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
266
268
  // injected from the SSH tab when opened via SFTP-UI button
267
269
  this.sshSession = null;
268
270
  this.profile = null;
271
+ this.recoveredStub = false;
269
272
  this.connecting = false;
270
273
  this.connected = false;
271
274
  // legacy UI fields kept for now (not used when opened from SSH tab)
@@ -353,9 +356,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
353
356
  // If there's no live SSH session, this tab was likely restored across
354
357
  // restart or opened in an invalid context. Close it immediately to avoid
355
358
  // an empty, nameless SFTP tab lingering after restart.
356
- if (!this.sshSession) {
359
+ if (this.recoveredStub || !this.sshSession) {
357
360
  try {
358
- this.app.closeTab(this);
361
+ this.app.closeTab(this.topmostParent ?? this);
359
362
  }
360
363
  catch (e) {
361
364
  console.error('[SFTP-UI] Failed to close invalid SFTP tab', e);
@@ -1591,7 +1594,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1591
1594
  getDefaultRemotePath() {
1592
1595
  const username = (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
1593
1596
  if (username) {
1594
- return `/home/${username}`;
1597
+ return username === 'root' ? `/root` : `/home/${username}`;
1595
1598
  }
1596
1599
  return '/';
1597
1600
  }
@@ -2036,12 +2039,16 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
2036
2039
  }
2037
2040
  super.destroy();
2038
2041
  }
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.
2042
+ // A custom recovery token ensures that a SplitTab wrapper won't become empty
2043
+ // after restart. On recovery we restore a stub SFTP tab which immediately
2044
+ // closes its `topmostParent` (the SplitTab wrapper).
2042
2045
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2043
2046
  async getRecoveryToken(_options) {
2044
- return null;
2047
+ return {
2048
+ type: _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_9__.SFTP_RECOVERY_TYPE,
2049
+ version: 1,
2050
+ state: {},
2051
+ };
2045
2052
  }
2046
2053
  async confirmDelete() {
2047
2054
  if (!this.deleteConfirmVisible) {
@@ -2339,388 +2346,388 @@ __decorate([
2339
2346
  SftpManagerTabComponent = __decorate([
2340
2347
  (0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
2341
2348
  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>
2349
+ template: `
2350
+ <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
2351
+ <div class="top-profiles" *ngIf="profile || recentProfiles.length">
2352
+ <div class="current" *ngIf="profile">
2353
+ <span class="label">Device:</span>
2354
+ <span class="value">{{ getProfileLabel(profile) }}</span>
2355
+ </div>
2356
+ <div class="recent" *ngIf="recentProfiles.length">
2357
+ <span class="label">Recent:</span>
2358
+ <button
2359
+ class="profile-chip"
2360
+ *ngFor="let p of recentProfiles"
2361
+ (click)="launchProfileFromSFTP(p)"
2362
+ >
2363
+ {{ getProfileLabel(p) }}
2364
+ </button>
2365
+ </div>
2366
+ </div>
2367
+ <div class="sftp-body">
2368
+ <div class="pane">
2369
+ <div class="pane-title">
2370
+ <div class="pane-label">Local</div>
2371
+ <div class="pane-path">
2372
+ <input
2373
+ [(ngModel)]="localPathInput"
2374
+ (keyup.enter)="goToLocalPathInput()"
2375
+ />
2376
+ </div>
2377
+ <div class="pane-actions">
2378
+ <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2379
+ <option value="">Go to…</option>
2380
+ <option *ngFor="let p of localPathPresets" [value]="p.id">
2381
+ {{ p.label }}
2382
+ </option>
2383
+ </select>
2384
+ <button
2385
+ class="fav-toggle"
2386
+ [class.active]="isCurrentFavorite()"
2387
+ (click)="toggleCurrentFavorite()"
2388
+ title="Toggle favorite for this path"
2389
+ >
2390
+
2391
+ </button>
2392
+ <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2393
+ <option value="">Favorites…</option>
2394
+ <option *ngFor="let f of localFavorites" [value]="f.id">
2395
+ {{ f.label }}
2396
+ </option>
2397
+ </select>
2398
+ <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2399
+ <button (click)="goToLocalPathInput()">Go</button>
2400
+ <button (click)="refreshLocal()">Refresh</button>
2401
+ </div>
2402
+ </div>
2403
+ <div class="pane-filters">
2404
+ <div class="breadcrumbs">
2405
+ <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2406
+ <button
2407
+ class="crumb-button"
2408
+ (click)="navigateLocalBreadcrumb(i)"
2409
+ (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2410
+ >
2411
+ {{ part.label }}
2412
+ </button>
2413
+ <span class="crumb-separator" *ngIf="!last">›</span>
2414
+ </ng-container>
2415
+ </div>
2416
+ <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2417
+ <label class="show-hidden-toggle">
2418
+ <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2419
+ <span>Show hidden</span>
2420
+ </label>
2421
+ </div>
2422
+ <div class="pane-list"
2423
+ (dragover)="onDragOver($event)"
2424
+ (drop)="onDropOnLocal($event)"
2425
+ >
2426
+ <div class="entry header">
2427
+ <span class="icon"></span>
2428
+ <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2429
+ <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2430
+ <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2431
+ </div>
2432
+ <div
2433
+ class="entry"
2434
+ *ngIf="canLocalUp()"
2435
+ (dblclick)="localUp()"
2436
+ >
2437
+ <span class="icon">⬆</span>
2438
+ <span class="name">Go up</span>
2439
+ <span class="size"></span>
2440
+ <span class="date"></span>
2441
+ </div>
2442
+ <div
2443
+ class="entry"
2444
+ *ngFor="let e of getFilteredLocalEntries()"
2445
+ (click)="selectLocal(e, $event)"
2446
+ (dblclick)="openLocal(e)"
2447
+ (mousedown)="onLocalMouseDown(e, $event)"
2448
+ (contextmenu)="onLocalContextMenu(e, $event)"
2449
+ (dragover)="onLocalEntryDragOver(e, $event)"
2450
+ (drop)="onLocalEntryDrop(e, $event)"
2451
+ [class.drop-target]="localDropActive"
2452
+ [class.selected]="isLocalSelected(e)"
2453
+ [draggable]="true"
2454
+ (dragstart)="onDragStartLocal($event, e)"
2455
+ >
2456
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2457
+ <span class="name">{{ e.name }}</span>
2458
+ <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2459
+ <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2460
+ </div>
2461
+ </div>
2462
+ <div class="pane-actions-bar">
2463
+ <div class="selection" *ngIf="selectedLocal.length">
2464
+ Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2465
+ </div>
2466
+ <div class="action-inputs">
2467
+ <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2468
+ <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2469
+ </div>
2470
+ <div class="action-buttons">
2471
+ <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2472
+ <button (click)="refreshLocal()">Refresh</button>
2473
+ <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2474
+ <button (click)="localNewFolder()">New Folder</button>
2475
+ <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2476
+ <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2477
+ </div>
2478
+ </div>
2479
+ </div>
2480
+
2481
+ <div class="pane">
2482
+ <div class="pane-title">
2483
+ <div class="pane-label">
2484
+ Remote
2485
+ <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2486
+ — {{ profile.options.host }}
2487
+ </span>
2488
+ </div>
2489
+ <div class="pane-path">
2490
+ <input
2491
+ [(ngModel)]="remotePathInput"
2492
+ (keyup.enter)="goToRemotePathInput()"
2493
+ [disabled]="!connected"
2494
+ />
2495
+ </div>
2496
+ <div class="pane-actions">
2497
+ <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2498
+ <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2499
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2500
+ </div>
2501
+ </div>
2502
+ <div class="pane-filters">
2503
+ <div class="breadcrumbs" *ngIf="connected">
2504
+ <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2505
+ <button
2506
+ class="crumb-button"
2507
+ (click)="navigateRemoteBreadcrumb(i)"
2508
+ >
2509
+ {{ part.label }}
2510
+ </button>
2511
+ <span class="crumb-separator" *ngIf="!last">›</span>
2512
+ </ng-container>
2513
+ </div>
2514
+ <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2515
+ <label class="show-hidden-toggle">
2516
+ <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2517
+ <span>Show hidden</span>
2518
+ </label>
2519
+ </div>
2520
+ <div class="pane-list"
2521
+ (dragover)="onDragOver($event)"
2522
+ (drop)="onDropOnRemote($event)"
2523
+ >
2524
+ <div class="entry dim" *ngIf="!connected">
2525
+ <span class="name">Not connected</span>
2526
+ </div>
2527
+ <div class="entry header" *ngIf="connected">
2528
+ <span class="icon"></span>
2529
+ <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2530
+ <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2531
+ <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2532
+ </div>
2533
+ <div
2534
+ class="entry"
2535
+ *ngIf="connected && remotePath !== '/'"
2536
+ (dblclick)="remoteUp()"
2537
+ >
2538
+ <span class="icon">⬆</span>
2539
+ <span class="name">Go up</span>
2540
+ <span class="size"></span>
2541
+ <span class="date"></span>
2542
+ </div>
2543
+ <div
2544
+ class="entry"
2545
+ *ngFor="let e of getFilteredRemoteEntries()"
2546
+ (click)="selectRemote(e, $event)"
2547
+ (dblclick)="openRemote(e)"
2548
+ (mousedown)="onRemoteMouseDown(e, $event)"
2549
+ (contextmenu)="onRemoteContextMenu(e, $event)"
2550
+ (dragover)="onRemoteEntryDragOver(e, $event)"
2551
+ (drop)="onRemoteEntryDrop(e, $event)"
2552
+ [class.drop-target]="remoteDropActive"
2553
+ [class.selected]="isRemoteSelected(e)"
2554
+ [draggable]="connected"
2555
+ (dragstart)="onDragStartRemote($event, e)"
2556
+ >
2557
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2558
+ <span class="name">{{ e.name }}</span>
2559
+ <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2560
+ <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2561
+ </div>
2562
+ </div>
2563
+ <div class="pane-actions-bar">
2564
+ <div class="selection" *ngIf="selectedRemote.length">
2565
+ Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2566
+ </div>
2567
+ <div class="action-inputs">
2568
+ <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2569
+ <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2570
+ </div>
2571
+ <div class="action-buttons">
2572
+ <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2573
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2574
+ <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2575
+ <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2576
+ <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2577
+ <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2578
+ <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2579
+ </div>
2580
+ </div>
2581
+ </div>
2582
+ </div>
2583
+ <div class="sftp-transfers" *ngIf="transfers.length">
2584
+ <div class="transfer" *ngFor="let t of transfers">
2585
+ <div class="transfer-main">
2586
+ <div class="transfer-title">
2587
+ <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2588
+ <span class="name">{{ t.name }}</span>
2589
+ </div>
2590
+ <div class="transfer-path">
2591
+ <span class="label">Remote:</span>
2592
+ <span class="value">{{ t.remotePath }}</span>
2593
+ </div>
2594
+ <div class="transfer-path">
2595
+ <span class="label">Local:</span>
2596
+ <span class="value">{{ t.localPath }}</span>
2597
+ </div>
2598
+ <div class="bar">
2599
+ <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2600
+ </div>
2601
+ </div>
2602
+ <div class="transfer-stats">
2603
+ <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2604
+ <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2605
+ <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2606
+ </div>
2607
+ </div>
2608
+ </div>
2609
+
2610
+ <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2611
+ <div class="delete-dialog">
2612
+ <div class="delete-text">{{ deleteConfirmText }}</div>
2613
+ <div class="delete-buttons">
2614
+ <button class="danger" (click)="confirmDelete()">Delete</button>
2615
+ <button (click)="cancelDelete()">Cancel</button>
2616
+ </div>
2617
+ </div>
2618
+ </div>
2619
+
2620
+ <div class="delete-overlay" *ngIf="replaceConfirmVisible">
2621
+ <div class="delete-dialog">
2622
+ <div class="delete-text">{{ replaceConfirmText }}</div>
2623
+ <div class="delete-buttons">
2624
+ <button class="danger" (click)="confirmReplace()">Replace</button>
2625
+ <button (click)="cancelReplace()">Cancel</button>
2626
+ </div>
2627
+ </div>
2628
+ </div>
2629
+
2630
+ <div class="delete-overlay" *ngIf="inputDialogVisible">
2631
+ <div class="delete-dialog" (click)="$event.stopPropagation()">
2632
+ <div class="delete-text">{{ inputDialogTitle }}</div>
2633
+ <input
2634
+ class="dialog-input"
2635
+ [(ngModel)]="inputDialogValue"
2636
+ [placeholder]="inputDialogPlaceholder"
2637
+ (keyup.enter)="confirmInputDialog()"
2638
+ />
2639
+ <div class="delete-buttons">
2640
+ <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2641
+ <button (click)="cancelInputDialog()">Cancel</button>
2642
+ </div>
2643
+ </div>
2644
+ </div>
2645
+
2646
+ <div
2647
+ class="local-menu"
2648
+ *ngIf="localMenuVisible"
2649
+ [style.left.px]="localMenuX"
2650
+ [style.top.px]="localMenuY"
2651
+ (click)="$event.stopPropagation()"
2652
+ >
2653
+ <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2654
+ {{ item.label }}
2655
+ </div>
2656
+ </div>
2657
+ </div>
2651
2658
  `,
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)); }
2659
+ styles: [`
2660
+ .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2661
+ 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; }
2662
+ button:disabled { opacity: 0.5; cursor: default; }
2663
+ .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2664
+ .top-profiles .current .label,
2665
+ .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2666
+ .top-profiles .value { font-weight: 600; }
2667
+ .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; }
2668
+ .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2669
+ .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2670
+ .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2671
+ .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); }
2672
+ .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2673
+ .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2674
+ .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; }
2675
+ .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; }
2676
+ .pane-actions { display: flex; gap: 8px; align-items: center; }
2677
+ .pane-actions .path-preset,
2678
+ .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; }
2679
+ .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2680
+ .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2681
+ .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; }
2682
+ .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2683
+ .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); }
2684
+ .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; }
2685
+ .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2686
+ .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2687
+ .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2688
+ .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; }
2689
+ .crumb-button:hover { background: rgba(255,255,255,0.10); }
2690
+ .crumb-separator { opacity: 0.6; }
2691
+ .pane-list { flex: 1; overflow: auto; padding: 4px; }
2692
+ .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; }
2693
+ .entry:hover { background: rgba(255,255,255,0.06); }
2694
+ .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2695
+ .entry.dim { opacity: 0.7; }
2696
+ .icon { text-align: center; opacity: 0.85; }
2697
+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2698
+ .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2699
+ .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2700
+ .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2701
+ .sortable { cursor: pointer; }
2702
+ .entry.selected { background: rgba(80,160,255,0.18); }
2703
+ .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); }
2704
+ .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2705
+ .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2706
+ .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; }
2707
+ .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2708
+ .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2709
+ .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; }
2710
+ .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2711
+ .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2712
+ .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2713
+ .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2714
+ .transfer-path .label { min-width: 48px; }
2715
+ .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; }
2716
+ .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2717
+ .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2718
+ .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2719
+ .transfer-stats .percent { font-weight: 600; }
2720
+ .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2721
+ .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2722
+ .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2723
+ .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; }
2724
+ .delete-text { font-size: 13px; }
2725
+ .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; }
2726
+ .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2727
+ .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2728
+ .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); }
2729
+ .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2730
+ .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2724
2731
  `],
2725
2732
  }),
2726
2733
  __metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
@@ -2731,6 +2738,57 @@ SftpManagerTabComponent = __decorate([
2731
2738
 
2732
2739
 
2733
2740
 
2741
+ /***/ },
2742
+
2743
+ /***/ "./src/sftp-recovery-provider.ts"
2744
+ /*!***************************************!*\
2745
+ !*** ./src/sftp-recovery-provider.ts ***!
2746
+ \***************************************/
2747
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
2748
+
2749
+ __webpack_require__.r(__webpack_exports__);
2750
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2751
+ /* harmony export */ SFTP_RECOVERY_TYPE: () => (/* binding */ SFTP_RECOVERY_TYPE),
2752
+ /* harmony export */ SftpTabRecoveryProvider: () => (/* binding */ SftpTabRecoveryProvider)
2753
+ /* harmony export */ });
2754
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
2755
+ /* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
2756
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
2757
+ /* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
2758
+ /* harmony import */ var _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-manager-tab.component */ "./src/sftp-manager-tab.component.ts");
2759
+ var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
2760
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2761
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
2762
+ 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;
2763
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2764
+ };
2765
+
2766
+
2767
+
2768
+ const SFTP_RECOVERY_TYPE = 'tabby-sftp-ui:sftp-tab';
2769
+ let SftpTabRecoveryProvider = class SftpTabRecoveryProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.TabRecoveryProvider {
2770
+ async applicableTo(recoveryToken) {
2771
+ return recoveryToken?.type === SFTP_RECOVERY_TYPE;
2772
+ }
2773
+ async recover(_recoveryToken) {
2774
+ // Restore a stub tab that immediately closes itself and its SplitTab wrapper.
2775
+ return {
2776
+ type: _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__.SftpManagerTabComponent,
2777
+ inputs: {
2778
+ sshSession: null,
2779
+ profile: null,
2780
+ recoveredStub: true,
2781
+ },
2782
+ };
2783
+ }
2784
+ };
2785
+ SftpTabRecoveryProvider = __decorate([
2786
+ (0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)()
2787
+ ], SftpTabRecoveryProvider);
2788
+
2789
+
2790
+
2791
+
2734
2792
  /***/ },
2735
2793
 
2736
2794
  /***/ "./src/sftp-terminal-decorator.ts"
@@ -2768,7 +2826,13 @@ let SftpTerminalDecorator = class SftpTerminalDecorator extends tabby_terminal__
2768
2826
  attach(terminal) {
2769
2827
  super.attach(terminal);
2770
2828
  // Best-effort DOM injection: place button near the existing Reconnect button if present.
2829
+ // Only inject for SSH tabs (with a live sshSession property).
2771
2830
  const tryInsert = () => {
2831
+ // Skip for local terminals (PowerShell, CMD, etc.) - only SSH tabs need this button.
2832
+ const sshSession = terminal.sshSession;
2833
+ if (!sshSession) {
2834
+ return false;
2835
+ }
2772
2836
  try {
2773
2837
  const host = terminal.element?.nativeElement ?? null;
2774
2838
  if (!host) {
@@ -3162,6 +3226,7 @@ __webpack_require__.r(__webpack_exports__);
3162
3226
  /* harmony import */ var _sftp_hotkey__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./sftp-hotkey */ "./src/sftp-hotkey.ts");
3163
3227
  /* harmony import */ var _sftp_ui_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp-ui.service */ "./src/sftp-ui.service.ts");
3164
3228
  /* harmony import */ var _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./sftp-terminal-decorator */ "./src/sftp-terminal-decorator.ts");
3229
+ /* harmony import */ var _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./sftp-recovery-provider */ "./src/sftp-recovery-provider.ts");
3165
3230
  var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
3166
3231
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3167
3232
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3181,6 +3246,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
3181
3246
 
3182
3247
 
3183
3248
 
3249
+
3184
3250
  let SftpUiModule = class SftpUiModule {
3185
3251
  constructor(_) { }
3186
3252
  };
@@ -3197,6 +3263,7 @@ SftpUiModule = __decorate([
3197
3263
  { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabContextMenuItemProvider, useClass: _sftp_context_menu__WEBPACK_IMPORTED_MODULE_6__.SftpContextMenuProvider, multi: true },
3198
3264
  { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.HotkeyProvider, useClass: _sftp_hotkey__WEBPACK_IMPORTED_MODULE_7__.SftpUiHotkeyProvider, multi: true },
3199
3265
  { provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_4__.TerminalDecorator, useClass: _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_9__.SftpTerminalDecorator, multi: true },
3266
+ { provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabRecoveryProvider, useClass: _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_10__.SftpTabRecoveryProvider, multi: true },
3200
3267
  _sftp_ui_service__WEBPACK_IMPORTED_MODULE_8__.SftpUiService,
3201
3268
  ],
3202
3269
  }),