tabby-rdp 0.5.1 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2
4
+
5
+ **Added**
6
+ - **Updates settings** include stable and preview channels (beta releases and release candidates), automatic and
7
+ manual checks, compatible version selection, rollback and uninstall controls. Changes require confirmation and
8
+ take effect after restarting Tabby. Update preferences and installation history stay on this computer.
9
+ - **Installation progress stays in the confirmation dialog**, with elapsed time and a verified success or failure
10
+ result. The dialog remains visible if Settings closes. The installed version is checked before reporting success.
11
+
12
+ **Fixed**
13
+ - **Commands through the system SSH report SSH's own failure** when it exits before reading their input,
14
+ instead of letting a broken-pipe error escape unhandled. Commands that ignore their input still return their output.
15
+ - **Plugin startup waits for Tabby's configuration** before migrating update preferences, avoiding an error that
16
+ could disable third-party plugins for the session.
17
+
18
+ **Changed**
19
+ - **Troubleshooting opens the Markdown guide** from the links at the top of Remote Desktop settings. The former
20
+ Troubleshooting tab is now Updates; connection logs are under Desktops. Empty lists use concise status messages.
21
+ - **Settings descriptions are shorter**, and help icons appear consistently after their descriptions.
22
+ - **Troubleshooting and desk cleanup guides are included in the npm package.**
23
+ - **Host compatibility checks share one layer**, with host and runtime versions, the host's declared xterm dependency,
24
+ and observed capabilities included in copied connection logs. Modal drag fallbacks and terminal input handling use
25
+ shared adapters that check the host's actual features, including on forks and nightly builds.
26
+ - **CI and the release workflow check out the code with actions/checkout 7.0.1** (#56), pinned to its commit as before,
27
+ in place of 4.4.0. Nothing in the package changes.
28
+
3
29
  ## 0.5.1
4
30
 
5
31
  Security fixes from an audit of 0.5.0: saved passwords and certificates, what a host says about its desktops, `desk`,
package/README.md CHANGED
@@ -20,8 +20,8 @@ Linux and Windows, side by side, through the SSH connections you already have.
20
20
  ---
21
21
 
22
22
  tabby-rdp is a plugin for the [Tabby](https://tabby.sh) terminal. Press a key in an SSH tab and the machine's desktop
23
- appears in its place; press it again and you're back at the shell. Everything travels inside the SSH connection:
24
- nothing is opened on the network, and nothing has to be installed on the remote machine beforehand.
23
+ appears in its place; press it again and you're back at the shell. SSH desktops use the connection you already have;
24
+ remote desktop profiles can also connect directly or through an RD Gateway.
25
25
 
26
26
  - **Linux desktops without a monitor or a login.** The plugin starts a headless GNOME session for your SSH user with
27
27
  GNOME Remote Desktop, on the fly, without root. A server in a rack works as well as a workstation.
@@ -40,8 +40,8 @@ nothing is opened on the network, and nothing has to be installed on the remote
40
40
  - **`desk`:** type `desk` in the console and the same shell, with its history and running programs, moves into a
41
41
  terminal on the desktop.
42
42
 
43
- Everything is in **Settings › Remote Desktop**, in tabs: getting started and the shortcuts, the settings, the name
44
- overlay, the desktops and certificates the plugin keeps, saved accounts, and troubleshooting.
43
+ Everything is in **Settings › Remote Desktop**: Getting started, Settings, Overlay, Desktops, Accounts and Updates.
44
+ The **Troubleshooting** link at the top opens the [troubleshooting guide](TROUBLESHOOTING.md).
45
45
 
46
46
  The remote desktop itself is [IronRDP](https://github.com/Devolutions/IronRDP)'s web client, running inside Tabby.
47
47
  Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([below](#built-on-ironrdp)).
@@ -49,6 +49,7 @@ Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([
49
49
  ## Contents
50
50
 
51
51
  - [Install](#install)
52
+ - [Versions and recovery](#versions-and-recovery)
52
53
  - [Use](#use)
53
54
  - [Features](#features)
54
55
  - [Windows and other desktops behind a host](#windows-and-other-desktops-behind-a-host)
@@ -78,6 +79,24 @@ Or from a terminal, into Tabby's plugin folder (on macOS `~/Library/Application
78
79
  npm install tabby-rdp
79
80
  ```
80
81
 
82
+ ## Versions and recovery
83
+
84
+ Open **Settings › Remote Desktop › Updates** to manage the installed version:
85
+
86
+ - **Stable** is the default. **Preview** also offers beta releases and release candidates when available.
87
+ - Automatic checks look for updates once a day. **Check now** checks manually; installation is always your choice.
88
+ - Choose an exact compatible version, **Return to stable**, or **Restore previous version** after an installation
89
+ from this page. Version changes currently support **0.5.1 or newer**.
90
+ - **Uninstall plugin…** removes the package. **Manage in Tabby…** opens Tabby's plugin controls to disable it.
91
+ Saved connections, settings, passwords and remote setup are kept.
92
+
93
+ The confirmation dialog stays open during installation or removal and shows progress, elapsed time and the verified
94
+ result. After success, close remote desktops and restart Tabby. Until then, Updates shows the running version and
95
+ the installed version waiting for restart. Preferences and version history stay on this computer.
96
+
97
+ See [troubleshooting and recovery](TROUBLESHOOTING.md#recovery) if the plugin cannot load or you want to clean up saved
98
+ data or remote helpers.
99
+
81
100
  ## Use
82
101
 
83
102
  Open an SSH tab to a Linux machine with GNOME, and:
@@ -371,7 +390,7 @@ the open desktops that follow it, takes back the files offered to them, and stop
371
390
  several files that is under way before its next step. Narrowing applies in full only once they reconnect: reconnect a
372
391
  desktop you don't trust that far. **Paste to all desktops** leaves out the desktops that don't take this computer's
373
392
  clipboard and says which. Shared folders are a setting of their own, and screenshots and **Copy log** (Settings ›
374
- Remote Desktop › Troubleshooting) still go to your clipboard: they are your own actions.
393
+ Remote Desktop › Desktops › Open desktops) still go to your clipboard: they are your own actions.
375
394
 
376
395
  ## Shared folders
377
396
 
@@ -495,7 +514,8 @@ desktop of a machine you don't trust with `desk` off, which also has Tabby forge
495
514
  ## Settings
496
515
 
497
516
  Open **Settings › Remote Desktop** in Tabby, right-click the desktop button in Tabby's header, or open **Settings** in the Remote Desktop section of a terminal's or tab's menu.
498
- They are stored in Tabby's config under `remoteDesktop`:
517
+ Desktop settings are stored in Tabby's config under `remoteDesktop`; update preferences and installation history
518
+ are local to this computer:
499
519
 
500
520
  | Setting | Default | |
501
521
  |---|---|---|
@@ -517,12 +537,12 @@ They are stored in Tabby's config under `remoteDesktop`:
517
537
  | Session backend (`sessionBackend`) | `native` | For `desk`: `native` (trd-pty) or `tmux`. Config file only. |
518
538
  | Find virtual machines on SSH hosts (`discoverVMs`) | On | Lists a host's libvirt VMs with a desktop in its menu ([VMs on a host](#vms-on-a-host)). |
519
539
  | Shut down VMs it started (`shutDownIdle`) | Never | 5, 15 or 60: minutes without a desktop open after which a VM the plugin started is shut down again ([VMs on a host](#vms-on-a-host)). |
520
- | Tell me about new versions (`checkUpdates`) | On | Once a day, asks npm for the latest tabby-rdp (nothing else is sent), and says so in a note, the menus and the settings page when there's a newer one: Tabby itself shows plugin upgrades only on its Plugins page. |
540
+ | Check for updates automatically (Updates tab) | On, Stable | Checks npm once a day; choose Stable or Preview, check manually, and install a compatible version explicitly. [Versions and recovery](#versions-and-recovery). |
521
541
  | Desktop name overlay (`osd`) | When it helps | `show` (`auto`, `always`, `off`), `font`, `size`, `position`, `color` (empty: white; else a color name, `#hex`, or a color function of numbers such as `rgb()`) and `seconds`; the settings page previews it, at a resolution you pick. |
522
542
 
523
543
  ## Requirements
524
544
 
525
- - **Tabby 1.0.236 or newer**, on macOS, Windows or Linux (tested with 1.0.236 and 1.0.237). On a fresh Windows, Tabby
545
+ - **Tabby 1.0.236 or newer**, on macOS, Windows or Linux. On a fresh Windows, Tabby
526
546
  itself needs the [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist)
527
547
  for SSH.
528
548
  - **Linux desktops:** GNOME Shell and GNOME Remote Desktop 46 or newer (Ubuntu 24.04, for example), a systemd user
@@ -536,7 +556,13 @@ They are stored in Tabby's config under `remoteDesktop`:
536
556
  host or running OpenSSH Server itself.
537
557
 
538
558
  Tested with Ubuntu 24.04 (GNOME Remote Desktop 46.3) and Windows 11 (Pro and Enterprise), from Tabby on macOS,
539
- Windows 11 and Ubuntu 24.04 (arm64).
559
+ Windows 11 and Ubuntu 24.04 (arm64). Recent host compatibility tests cover official Tabby **1.0.238** on macOS
560
+ with xterm 5, and upstream xterm 6 builds on macOS and Linux. The new package installation dialog is tested on
561
+ macOS with Tabby 1.0.238; Windows qualification of these changes remains pending. See the
562
+ [host test matrix](test/README.md#host-qualification) for the exact builds and coverage.
563
+
564
+ The plugin checks the host's available features through a shared compatibility layer. Connection logs include the
565
+ host and runtime versions, declared xterm dependency and observed capabilities to help diagnose forks and nightlies.
540
566
 
541
567
  ## Built on IronRDP
542
568
 
@@ -573,8 +599,10 @@ it, not just this plugin.
573
599
  ## Development
574
600
 
575
601
  ```sh
576
- npm install
602
+ npm ci --ignore-scripts
577
603
  npm run build # TypeScript to dist/
604
+ npm run typecheck # plugin and test TypeScript
605
+ npm run test:unit # standard unit suite; no remote machine needed
578
606
  scripts/sandbox.sh # a separate Tabby with this plugin linked in (macOS), DevTools on port 9334
579
607
  npm test # end-to-end suites against a test machine: see test/README.md
580
608
  npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches: see ironrdp/README.md
@@ -626,7 +654,8 @@ npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches:
626
654
 
627
655
  Questions ("how do I…", "is this expected?") go to [Discussions › Q&A](https://github.com/meanaverage/tabby-rdp/discussions/categories/q-a);
628
656
  bugs and feature requests to the [issues](https://github.com/meanaverage/tabby-rdp/issues). A bug report is most useful
629
- with the connection log: **Settings › Remote Desktop › Troubleshooting › Open desktops › Copy log**.
657
+ with the connection log: **Settings › Remote Desktop › Desktops › Open desktops › Copy log**.
658
+ Start with the [troubleshooting guide](TROUBLESHOOTING.md) for common connection and installation problems.
630
659
 
631
660
  ## License
632
661
 
@@ -0,0 +1,77 @@
1
+ # Troubleshooting
2
+
3
+ For connection logs, open **Settings › Remote Desktop › Desktops › Open desktops**, then choose **Copy log**. In version 0.5.1, the list is in **Troubleshooting**. Review logs before sharing them.
4
+
5
+ <a id="certificate"></a>
6
+
7
+ ## A certificate this computer can't verify, or one that has changed
8
+
9
+ A desktop you connect to directly (a remote desktop profile, an imported file) and an RD Gateway connect without asking when their certificate is valid for their name by this computer's certificate authorities, as an organisation's own desktops and gateways often are. Any other is shown the first time, with why it can't be verified (most often: it is the server's own, self-signed) and its fingerprint, before anything of your sign-in is sent: check the fingerprint with whoever runs it, some other way than this connection (it is also in the desktop's connection log), then choose **Trust the certificate**. The question also says whom the certificate says it was issued to and by, and when it is valid: its own word, which nothing has checked, unless its chain leads to an authority this computer trusts (another machine's certificate then shows that machine's name) and the TLS library didn't refuse it for a reason of its own (a limit set on the authority, say). An issuer you wouldn't expect there (one that inspects your network's connections, say) is worth asking about before you trust it, but the one you would expect proves nothing: anyone can make a certificate that names it. Only the fingerprint, checked some other way, does. A desktop behind an SSH host remembers its certificate on first use without asking, since the way there runs inside SSH. When a remembered certificate differs later, the plugin stops before signing in, so nothing of your sign-in has gone to that server; only a certificate an authority vouched for is replaced without a question, by another it vouches for too. Reinstalling the machine or renewing its certificate changes it: then choose **Trust the new certificate**. If neither happened, something else is answering at that address. A GNOME desktop only ever accepts the certificate the plugin made for it; a different one there means another program listens on its port. Remembered certificates are in Settings › Remote Desktop › Desktops.
10
+
11
+ <a id="sign-in"></a>
12
+
13
+ ## Wrong user name or password
14
+
15
+ Windows and xrdp desktops sign in with an account on that machine (`DOMAIN\user` works). A saved password that no longer works is asked for again; **Sign in again…** in the tab's menu replaces it on purpose. xrdp doesn't refuse a wrong password: it shows its own login window instead. GNOME desktops never ask: the plugin manages their sign-in.
16
+
17
+ <a id="no-desktop"></a>
18
+
19
+ ## No desktop found on a Linux host
20
+
21
+ The plugin uses GNOME Remote Desktop 46 or newer (Ubuntu 24.04, for example), with no root and no login screen. For KDE, XFCE, MATE and others, install xrdp and a desktop for its sessions: on Debian and Ubuntu, `sudo apt install xrdp xfce4`. A GNOME session already shared from the machine's own screen (GNOME's Desktop Sharing) has to be turned off first.
22
+
23
+ <a id="unreachable"></a>
24
+
25
+ ## Can't reach a Windows desktop
26
+
27
+ Windows needs Remote Desktop turned on (Settings › System › Remote Desktop; Pro, Enterprise or Server). Behind an SSH host, the address is as that host sees it: from there, `nc -z <address> 3389` should succeed. For a VM or machine that may be off, give the desktop a VM name or MAC address in its edit form and it is started when opened.
28
+
29
+ <a id="reconnecting"></a>
30
+
31
+ ## It keeps reconnecting
32
+
33
+ After sleep or a network change the desktop reconnects by itself, with growing pauses, and waits while the SSH connection is down (**Reconnect SSH** hurries that along). It stops after a few tries or on **Stop**. If it drops again right after connecting, check the host's free memory and that the desktop's session is still running there.
34
+
35
+ <a id="picture"></a>
36
+
37
+ ## The picture is blurry or slow
38
+
39
+ **Sharpness › Retina** renders at your screen's device pixels, with the remote's scale set to match. Video (H.264) is used when the remote sends it: Windows does, GNOME only with a hardware encoder (VA-API or NVENC). **Show connection status** shows the frame rate, the round trip and how the picture comes.
40
+
41
+ <a id="sound"></a>
42
+
43
+ ## No sound, or the microphone isn't heard
44
+
45
+ Sound and microphone changes apply on the next connection. xrdp needs `pipewire-module-xrdp` or `pulseaudio-module-xrdp`. The microphone needs the system's permission here (macOS: System Settings › Privacy & Security › Microphone, then restart Tabby). On GNOME, apps record from "Remoteaudio Source"; on Windows, the machine must allow audio recording redirection. A desktop whose microphone was stopped (**Stop** in the microphone's menu in Tabby's header, or **Send the microphone** unchecked in the desktop's menu) gets none, its reconnects included, until **Send the microphone** is checked again in its menu or that Tabby window is closed.
46
+
47
+ <a id="nested-ssh"></a>
48
+
49
+ ## I typed ssh to another machine in a terminal
50
+
51
+ In an SSH tab (or a split pane) where you typed `ssh` on to another machine, **Desktop** opens that machine's desktop, going through the first one, and `desk` works there too. Desktop asks which desktop you mean, since which `ssh` runs there is the first machine's to say; pick the other machine's "from now on" to have it open at once the next times. Desktops you set up behind a host open in a tab connected to that host, not through another one. The first machine has to log in to the other by itself for this: with a key there, or agent forwarding, as `ssh -o BatchMode=yes` would. Tabby's **Reconnect** belongs to the tab: it reconnects the first machine and ends the ssh typed in it. To have both desktops side by side, open the first machine's desktop in one pane, and ssh on in the other.
52
+
53
+ <a id="extra-monitor"></a>
54
+
55
+ ## The desktop is open somewhere else
56
+
57
+ GNOME Remote Desktop's headless mode gives each connection a screen of its own: a second computer, or another Tabby window, connected to the same account gets an empty extra screen rather than the same one. So when the desktop is open somewhere else already, you choose. **Take it over** disconnects the other connection, as Windows does; the session and its apps carry on, on this screen. **Open a second screen** keeps both, each with a screen of its own. A desktop taken over from here doesn't take itself back: it says so, and offers the same two choices.
58
+
59
+ <a id="recovery"></a>
60
+
61
+ ## Recover a version, disable the plugin, or remove it
62
+
63
+ In **0.5.2 or newer**, open **Settings › Remote Desktop › Updates**. **Stable** is the default; **Preview** includes beta releases and release candidates when available. Choose an exact published version and confirm installation. **Return to stable** can install an older version. **Restore previous version** uses the version saved after the last successful installation from this page. On older releases, manage the package in **Tabby › Settings › Plugins**.
64
+
65
+ The confirmation dialog stays open through progress and the verified result. If installation fails, the dialog shows the cause. After success, close remote desktops and restart Tabby; Updates shows the running version and the installed version waiting for restart until then.
66
+
67
+ Use **0.5.1 or newer** with current account and certificate settings, and respect any higher minimum documented by a later data migration.
68
+
69
+ If the plugin cannot load:
70
+
71
+ 1. Open **Tabby › Settings › Plugins**. Disable **tabby-rdp** to stop it loading, or uninstall and reinstall it there. Restart Tabby after changing plugins. Tabby’s [configuration locations](https://github.com/Eugeny/tabby/wiki/Config-file) help locate your profile when the application cannot open.
72
+ 2. For a specific known compatible version, open a terminal in **Tabby's plugin folder** (the folder containing the plugin installation's `package.json`, with `node_modules/tabby-rdp` below it). Run `npm install tabby-rdp@0.5.1`, replacing `0.5.1` with your known compatible exact version. Do not run this in the tabby-rdp source checkout, the application bundle, or inside `node_modules/tabby-rdp` itself. Fully quit and reopen Tabby afterwards.
73
+ 3. If a later version has migrated saved data, respect that version's documented minimum. Reinstall a compatible stable version or the last working preview; do not force an older version across a migration boundary.
74
+
75
+ Disabling or uninstalling keeps desktops, profiles, settings, remembered certificates and saved credentials. It also leaves any remote helpers and remote applications in place. To remove saved entries, use **Remote Desktop › Desktops** and **Accounts**, and **Profiles & connections** for profiles, before uninstalling. Unlock the credential store first if required; check any reported failure rather than assuming a credential was deleted. Data synced to other computers may need attention there too.
76
+
77
+ For the optional `desk` remote helper, turn **Settings › Remote Desktop › Settings › desk: bring the console along** off and reconnect to each host while the plugin is still installed. The next connection removes the helper's own files and login hook; an offline host cannot be cleaned this way until it returns. Read the [desk cleanup details](docs/desk.md) for manual removal and read-only startup files. End remote applications you no longer need separately. Uninstalling this plugin does not shut down remote hosts or erase their files.
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountRevisions = exports.ACCOUNT_REMEMBER_UNAVAILABLE = void 0;
4
+ const crypto_1 = require("crypto");
5
+ /** No secrets: invalidates old sign-in forms in other windows before an account edit touches the store. */
6
+ const PREFIX = 'tabby-rdp.account-revision.';
7
+ exports.ACCOUNT_REMEMBER_UNAVAILABLE = 'To remember this password, save the account again in Settings and reopen this sign-in form.';
8
+ class AccountRevisions {
9
+ constructor(storage = () => window.localStorage, coordinated = () => !!globalThis.navigator?.locks) {
10
+ this.storage = storage;
11
+ this.coordinated = coordinated;
12
+ }
13
+ /** A prompt may remember only when this window's account agrees with the shared revision. */
14
+ capture(account) {
15
+ const revision = account.credentialRevision ?? '';
16
+ return this.current(account.id, revision) ? revision : undefined;
17
+ }
18
+ current(id, revision) {
19
+ if (revision === undefined || !this.coordinated()) {
20
+ return false;
21
+ }
22
+ try {
23
+ const shared = this.storage().getItem(PREFIX + id);
24
+ return shared === null || shared === revision;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /** Synchronous publication: older writers fail their next check, including one already waiting on a store. */
31
+ change(id) {
32
+ if (!this.coordinated()) {
33
+ throw new Error('Saved account changes are unavailable. Restart or update Tabby and try again.');
34
+ }
35
+ const revision = (0, crypto_1.randomUUID)();
36
+ // A failed write must stop the edit: a revision held only in this window cannot invalidate another's form.
37
+ try {
38
+ this.storage().setItem(PREFIX + id, revision);
39
+ }
40
+ catch {
41
+ throw new Error('Saved account changes are unavailable. Restart Tabby and try again.');
42
+ }
43
+ return revision;
44
+ }
45
+ }
46
+ exports.AccountRevisions = AccountRevisions;
package/dist/accounts.js CHANGED
@@ -16,7 +16,8 @@ function accountsOf(store) {
16
16
  }
17
17
  seen.add(id);
18
18
  const domain = typeof a.domain === 'string' && a.domain.trim() ? a.domain.trim() : undefined;
19
- return [{ id, name: typeof a.name === 'string' && a.name.trim() ? a.name.trim() : signInName({ username, domain }), username, ...domain ? { domain } : {} }];
19
+ return [{ id, name: typeof a.name === 'string' && a.name.trim() ? a.name.trim() : signInName({ username, domain }), username,
20
+ ...domain ? { domain } : {}, ...typeof a.credentialRevision === 'string' ? { credentialRevision: a.credentialRevision } : {} }];
20
21
  });
21
22
  }
22
23
  /** A new account's id: nothing in it but that it is unused. */
@@ -29,6 +29,7 @@ const microphone_1 = require("./microphone");
29
29
  const drives_1 = require("./drives");
30
30
  const desktopForm_1 = require("./desktopForm");
31
31
  const accounts_1 = require("./accounts");
32
+ const accountRevisions_1 = require("./accountRevisions");
32
33
  const accountForm_1 = require("./accountForm");
33
34
  const fileTransfer_1 = require("./fileTransfer");
34
35
  const desktops_1 = require("./desktops");
@@ -45,6 +46,7 @@ const gateway_1 = require("./gateway");
45
46
  const authorities_1 = require("./authorities");
46
47
  const vms_1 = require("./vms");
47
48
  const sessionLog_1 = require("./sessionLog");
49
+ const hostCompat_1 = require("./hostCompat");
48
50
  const unshowable_1 = require("./unshowable");
49
51
  /**
50
52
  * The keychain entry that marks the passwords 0.5.0 saved for desktops reached through a gateway forgotten from this
@@ -348,7 +350,7 @@ class DesktopSession {
348
350
  setVisible(visible) {
349
351
  this.visible = visible;
350
352
  this.overlay.style.display = visible ? '' : 'none';
351
- this.container.querySelectorAll('textarea.xterm-helper-textarea').forEach(input => {
353
+ (0, hostCompat_1.terminalInputs)(this.container).forEach(input => {
352
354
  input.disabled = visible;
353
355
  });
354
356
  if (visible) {
@@ -478,6 +480,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
478
480
  /** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops; and what the scans learnt of the host. */
479
481
  this.vms = new Map();
480
482
  this.lastUsed = new Map();
483
+ this.accountRevisions = new accountRevisions_1.AccountRevisions();
481
484
  /** Each pane's `desk` requests, one at a time (see DeskGate). */
482
485
  this.deskGates = new WeakMap();
483
486
  /**
@@ -833,6 +836,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
833
836
  accounts() {
834
837
  return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
835
838
  }
839
+ accountCurrent(key, name, revision) {
840
+ const account = this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === key);
841
+ return !!account && (0, accounts_1.signInName)(account) === name && (account.credentialRevision ?? '') === revision && this.accountRevisions.current(account.id, revision);
842
+ }
836
843
  /**
837
844
  * The desktops that sign in with a saved account, or sign in to their RD Gateway with it: configured ones behind
838
845
  * SSH hosts, and RDP profiles (their own choice, or their profile group's default when they make none). Each comes
@@ -863,14 +870,31 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
863
870
  username: account.username.trim(),
864
871
  ...account.domain?.trim() ? { domain: account.domain.trim() } : {},
865
872
  };
873
+ let revision;
874
+ try {
875
+ revision = this.accountRevisions.change(entry.id);
876
+ }
877
+ catch (error) {
878
+ return { id: entry.id, keychainError: error?.message ?? String(error) };
879
+ }
880
+ entry.credentialRevision = revision;
881
+ // Invalidate before any store await. Keep the old sign-in name until changing its password succeeds.
882
+ if (old) {
883
+ this.config.store.remoteDesktop.accounts = list.map(a => a.id === old.id ? { ...a, credentialRevision: revision } : a);
884
+ await this.config.save();
885
+ }
886
+ const current = () => this.accountRevisions.current(entry.id, revision) && (!old || this.accounts().some(a => a.id === entry.id && a.credentialRevision === revision));
866
887
  // The password first: a new user name must not be written while the old password (for the old name) stays, nor
867
888
  // a new password reported as saved when the store refused it.
868
889
  let keychainError;
869
890
  if (password !== undefined && password !== '') {
870
- await (0, signin_1.saveCredentials)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }, `saved account ${entry.name}`).catch(e => { keychainError = String(e?.message ?? e); });
891
+ await (0, signin_1.saveCredentialsIf)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }, current, `saved account ${entry.name}`).catch(e => { keychainError = String(e?.message ?? e); });
871
892
  }
872
893
  else if (old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
873
- await (0, signin_1.forgetCredentialsOrFail)((0, accounts_1.accountKey)(entry.id)).catch(e => { keychainError = `the old password couldn't be removed: ${e?.message ?? e}`; });
894
+ await (0, signin_1.forgetCredentialsIf)((0, accounts_1.accountKey)(entry.id), current).catch(e => { keychainError = `the old password couldn't be removed: ${e?.message ?? e}`; });
895
+ }
896
+ if (!current()) {
897
+ return { id: entry.id, keychainError: 'The account changed again while this edit was being saved. The newer change was kept.' };
874
898
  }
875
899
  if (keychainError && !old) {
876
900
  // Not added at all: a retry would otherwise make a second account.
@@ -886,7 +910,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
886
910
  delete entry.domain;
887
911
  }
888
912
  }
889
- this.config.store.remoteDesktop.accounts = old ? list.map(a => a === old ? entry : a) : [...list, entry];
913
+ const now = this.accounts();
914
+ this.config.store.remoteDesktop.accounts = old ? now.map(a => a.id === old.id ? entry : a) : [...now, entry];
890
915
  await this.config.save();
891
916
  this.changed$.next();
892
917
  return { id: entry.id, keychainError };
@@ -923,11 +948,18 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
923
948
  if (response !== 0) {
924
949
  return false;
925
950
  }
926
- await this.removeAccount(id);
927
- return true;
951
+ try {
952
+ await this.removeAccount(id);
953
+ return true;
954
+ }
955
+ catch (error) {
956
+ this.notifications.error(`The account could not be fully removed: ${error?.message ?? error}`);
957
+ return false;
958
+ }
928
959
  }
929
960
  /** Removes a saved account and its password; desktops that used it go back to asking. */
930
961
  async removeAccount(id) {
962
+ const revision = this.accountRevisions.change(id);
931
963
  const store = this.config.store.remoteDesktop;
932
964
  store.accounts = this.accounts().filter(a => a.id !== id);
933
965
  if (this.configuredDesktops().some(d => (0, desktops_1.isDesktopEntry)(d) && (d.account === id || d.gatewayAccount === id))) {
@@ -964,7 +996,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
964
996
  }
965
997
  await this.config.save();
966
998
  this.changed$.next();
967
- await (0, signin_1.forgetCredentialsOrFail)((0, accounts_1.accountKey)(id)).catch(e => this.notifications.error(`The account is removed, but its password could not be: ${e?.message ?? e}`));
999
+ await (0, signin_1.forgetCredentialsIf)((0, accounts_1.accountKey)(id), () => this.accountRevisions.current(id, revision)).catch(e => this.notifications.error(`The account is removed, but its password could not be: ${e?.message ?? e}`));
968
1000
  }
969
1001
  /** `remoteDesktop.desktops`: the desktops configured behind SSH hosts. */
970
1002
  configuredDesktops() {
@@ -2721,6 +2753,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
2721
2753
  }
2722
2754
  session.status('');
2723
2755
  const mark = (0, signin_1.forgetMark)();
2756
+ const accountRevision = account && this.accountRevisions.capture(account);
2724
2757
  // The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via". A machine reached
2725
2758
  // with ssh typed in a host's console says which host: that host is what leads there.
2726
2759
  const hostOwn = spec.id === desktops_1.OWN_DESKTOP || spec.kind === 'xrdp' && spec.host === '127.0.0.1';
@@ -2729,8 +2762,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
2729
2762
  title: spec.hyperv ? `Sign in to ${target.label}${target.via ? ` (via ${target.via})` : ''}, to open ${spec.name}`
2730
2763
  : hostOwn && !target.via || target.direct ? `Sign in to ${spec.name}` : `Sign in to ${spec.name} (via ${via})`,
2731
2764
  username: account ? (0, accounts_1.signInName)(account) : spec.username,
2732
- error: retryError,
2733
- canRemember: true,
2765
+ error: [retryError, account && accountRevision === undefined ? accountRevisions_1.ACCOUNT_REMEMBER_UNAVAILABLE : ''].filter(Boolean).join(' '),
2766
+ canRemember: !account || accountRevision !== undefined,
2734
2767
  account: account?.name,
2735
2768
  }, session.disposed);
2736
2769
  // Only in the pane in front, as when connected (see run()): the form gets the keyboard when its pane does.
@@ -2741,7 +2774,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
2741
2774
  return entered && {
2742
2775
  host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, domain: account ? account.domain ?? '' : undefined,
2743
2776
  saveKey: account && (0, accounts_1.accountKey)(account.id), saveLabel: account && `saved account ${account.name}`, saveFor: account && (0, accounts_1.signInName)(account),
2744
- hyperv, mark,
2777
+ hyperv, mark, accountRevision,
2745
2778
  };
2746
2779
  }
2747
2780
  async connect(pane, target, spec, session) {
@@ -2895,7 +2928,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
2895
2928
  session.log.push('gateway: its saved account no longer exists; signing in with the desktop\'s');
2896
2929
  return undefined;
2897
2930
  }
2898
- const signIn = { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account) };
2931
+ const signIn = { domain: account.domain, saveKey: (0, accounts_1.accountKey)(account.id), saveLabel: `saved account ${account.name}`, saveFor: (0, accounts_1.signInName)(account), accountRevision: this.accountRevisions.capture(account) };
2899
2932
  const saved = retryError ? null : await (0, signin_1.loadCredentials)(signIn.saveKey);
2900
2933
  if (saved && saved.username === (0, accounts_1.signInName)(account)) {
2901
2934
  session.log.push(`gateway: the saved account "${account.name}"`);
@@ -2905,8 +2938,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
2905
2938
  const asked = (0, signin_1.askCredentials)(session.overlay, {
2906
2939
  title: `Sign in to the gateway ${gateway.host}, to reach ${spec.name}`,
2907
2940
  username: (0, accounts_1.signInName)(account),
2908
- error: retryError,
2909
- canRemember: true,
2941
+ error: [retryError, signIn.accountRevision === undefined ? accountRevisions_1.ACCOUNT_REMEMBER_UNAVAILABLE : ''].filter(Boolean).join(' '),
2942
+ canRemember: signIn.accountRevision !== undefined,
2910
2943
  account: account.name,
2911
2944
  }, session.disposed);
2912
2945
  // Only in the pane in front (see endpointFor).
@@ -2930,10 +2963,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
2930
2963
  // Entered a moment ago and taken by the gateway: kept, while the account is still what it was signed in as.
2931
2964
  if (own?.remember) {
2932
2965
  own.remember = false;
2933
- const accountNow = this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === own.saveKey);
2934
- if (accountNow && (0, accounts_1.signInName)(accountNow) === own.saveFor) {
2935
- (0, signin_1.saveCredentials)(own.saveKey, own.credentials, own.saveLabel).then(() => session.log.push(`gateway: saved as the account's password in ${(0, signin_1.storeName)()}`), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
2936
- }
2966
+ (0, signin_1.saveCredentialsIf)(own.saveKey, own.credentials, () => this.accountCurrent(own.saveKey, own.saveFor, own.accountRevision), own.saveLabel).then(saved => session.log.push(saved ? `gateway: saved as the account's password in ${(0, signin_1.storeName)()}` : 'gateway: not saved: the account changed meanwhile'), e => session.log.push(`gateway: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
2937
2967
  }
2938
2968
  return stream;
2939
2969
  }
@@ -3564,11 +3594,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
3564
3594
  this.changed$.next();
3565
3595
  // Under a saved account only while that account is still there with the same sign-in name: the prompt may
3566
3596
  // have been open while it was removed or changed in Settings.
3567
- const accountNow = endpoint.saveKey ? this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === endpoint.saveKey) : undefined;
3568
- if (endpoint.remember && endpoint.saveKey && (!accountNow || (0, accounts_1.signInName)(accountNow) !== endpoint.saveFor)) {
3569
- session.log.push(`sign-in: not saved: the account ${accountNow ? 'changed' : 'was removed'} meanwhile`);
3597
+ if (endpoint.remember && endpoint.saveKey) {
3598
+ const key = endpoint.saveKey;
3599
+ (0, signin_1.saveCredentialsIf)(key, credentials, () => this.accountCurrent(key, endpoint.saveFor, endpoint.accountRevision), endpoint.saveLabel).then(saved => session.log.push(saved ? `sign-in: saved as the account's password in ${(0, signin_1.storeName)()}` : 'sign-in: not saved: the account changed meanwhile'), e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
3570
3600
  }
3571
- else if (endpoint.remember && !endpoint.saveKey && endpoint.mark !== undefined && (0, signin_1.forgottenSince)(session.credentialKey, endpoint.mark)) {
3601
+ else if (endpoint.remember && endpoint.mark !== undefined && (0, signin_1.forgottenSince)(session.credentialKey, endpoint.mark)) {
3572
3602
  // Edited (its user name, gateway or address) or removed while the server delayed the sign-in: what was
3573
3603
  // forgotten then stays forgotten, and the desktop asks the next time.
3574
3604
  session.log.push('sign-in: not saved: the desktop was changed or removed meanwhile');
@@ -3647,7 +3677,7 @@ exports.RemoteDesktopService = RemoteDesktopService = RemoteDesktopService_1 = _
3647
3677
  tabby_core_1.PlatformService])
3648
3678
  ], RemoteDesktopService);
3649
3679
  let styleInstalled = false;
3650
- function installStyle() {
3680
+ function installStyle(compat) {
3651
3681
  if (styleInstalled) {
3652
3682
  return;
3653
3683
  }
@@ -3655,7 +3685,7 @@ function installStyle() {
3655
3685
  const style = document.createElement('style');
3656
3686
  style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE + DRAGBAR_STYLE + accountForm_1.STYLE;
3657
3687
  document.head.appendChild(style);
3658
- keepWindowDraggable();
3688
+ keepWindowDraggable(compat);
3659
3689
  }
3660
3690
  /** A strip the height of the tab bar that stays a window drag region over a dialog (the plugin's, and Tabby's). */
3661
3691
  const DRAGBAR_STYLE = `
@@ -3663,12 +3693,14 @@ const DRAGBAR_STYLE = `
3663
3693
  .modal > .trd-form-dragbar { display: block; position: fixed; left: 0; right: 0; top: 0; height: var(--tabs-height, 38px); -webkit-app-region: drag; }
3664
3694
  `;
3665
3695
  /**
3666
- * Tabby's dialogs (ng-bootstrap modals) cover the whole window, tab bar included, with nothing marked as a drag
3667
- * region, so the window can't be moved while one shows. Each gets a strip where the tab bar is; the dialog box itself
3668
- * is below it. The plugin's own dialogs do the same (see settingsPage.ts).
3696
+ * Older Tabby dialogs cover the tab bar without a drag region. Supply their fallback strip, deferring to the host's
3697
+ * modal pseudo-element when it provides one. The plugin's own dialogs still need their strips (see settingsPage.ts).
3669
3698
  */
3670
- function keepWindowDraggable() {
3699
+ function keepWindowDraggable(compat) {
3671
3700
  const strip = (modal) => {
3701
+ if (compat.hasNativeModalDragRegion(modal)) {
3702
+ return;
3703
+ }
3672
3704
  if (!modal.querySelector(':scope > .trd-form-dragbar')) {
3673
3705
  const bar = document.createElement('div');
3674
3706
  bar.className = 'trd-form-dragbar';
@@ -10,6 +10,7 @@ const desktops_1 = require("./desktops");
10
10
  const gateway_1 = require("./gateway");
11
11
  const osd_1 = require("./osd");
12
12
  const wake_1 = require("./wake");
13
+ const hostCompat_1 = require("./hostCompat");
13
14
  /** host:port, [v6]:port, or a bare host (RDP's 3389); null if it doesn't look like an address. */
14
15
  function parseAddress(text) {
15
16
  // No '#' in a host: session and trust keys are split on it (see keyParts).
@@ -134,7 +135,7 @@ function askDesktop(pane, options) {
134
135
  }
135
136
  pane.appendChild(overlay);
136
137
  // Same as a desktop layer: the terminal underneath can't take the keyboard while the form shows.
137
- const inputs = Array.from(pane.querySelectorAll('textarea.xterm-helper-textarea')).filter(t => !t.disabled);
138
+ const inputs = (0, hostCompat_1.terminalInputs)(pane).filter(t => !t.disabled);
138
139
  inputs.forEach(t => { t.disabled = true; });
139
140
  const focusForm = () => {
140
141
  if (!form.contains(document.activeElement)) {