tabby-rdp 0.3.4 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,8 @@ const audio_1 = require("./audio");
25
25
  const connectionStatus_1 = require("./connectionStatus");
26
26
  const microphone_1 = require("./microphone");
27
27
  const desktopForm_1 = require("./desktopForm");
28
+ const accounts_1 = require("./accounts");
29
+ const accountForm_1 = require("./accountForm");
28
30
  const fileTransfer_1 = require("./fileTransfer");
29
31
  const desktops_1 = require("./desktops");
30
32
  const help_1 = require("./help");
@@ -322,6 +324,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
322
324
  * long as Tabby runs; a Windows host is found out on the first connect there, which costs a failed setup.
323
325
  */
324
326
  this.ownDesktops = new Map();
327
+ /**
328
+ * Forgets the saved account of the pane's desktop and connects again, with the sign-in form. For xrdp above all:
329
+ * with a wrong password it shows its own login window rather than refusing, so the form never comes back.
330
+ */
331
+ this.askAgain = new WeakSet();
325
332
  /** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops. */
326
333
  this.vms = new Map();
327
334
  this.lastUsed = new Map();
@@ -389,14 +396,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
389
396
  const spec = this.sessions.get(pane)?.spec;
390
397
  return !!spec && this.signsIn(spec);
391
398
  }
392
- /**
393
- * Forgets the saved account of the pane's desktop and connects again, with the sign-in form. For xrdp above all:
394
- * with a wrong password it shows its own login window rather than refusing, so the form never comes back.
395
- */
396
399
  async signInAgain(pane) {
397
400
  const session = this.sessions.get(pane);
398
401
  if (session && this.signsIn(session.spec)) {
399
402
  await (0, signin_1.forgetCredentials)(session.key);
403
+ // With a saved account, the form asks for its password anew (the saved one stays until then).
404
+ this.askAgain.add(pane);
400
405
  await this.reopen(pane, session.spec);
401
406
  }
402
407
  }
@@ -408,9 +413,10 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
408
413
  if (target.direct) {
409
414
  return [target.direct];
410
415
  }
416
+ // With their group's defaults (an account for the whole group), as Tabby resolves a profile when opening it.
411
417
  const viaProfile = target.profileId
412
418
  ? (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && p.options?.host && p.options.via === target.profileId)
413
- .map((p) => ({ ...p.options, name: p.name }))
419
+ .map((p) => ({ ...this.profiles.getConfigProxyForProfile(p).options, name: p.name }))
414
420
  : [];
415
421
  const specs = (0, desktops_1.desktopsFor)(target, this.config.store.remoteDesktop?.desktops, this.ownDesktops.get(target.key), viaProfile);
416
422
  // Found VMs come last, and not where a configured desktop has the same address.
@@ -480,6 +486,129 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
480
486
  desktopOf(pane) {
481
487
  return this.sessions.get(pane)?.spec ?? null;
482
488
  }
489
+ /** `remoteDesktop.accounts`: the saved accounts desktops can sign in with (see accounts.ts). */
490
+ accounts() {
491
+ return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
492
+ }
493
+ /**
494
+ * The desktops that sign in with a saved account: configured ones behind SSH hosts, and RDP profiles (their own
495
+ * choice, or their profile group's default when they make none). Each comes with what opens its editor.
496
+ */
497
+ accountUses(id) {
498
+ const desktops = this.configuredDesktops().flatMap((d, i) => d.account === id ? [{ name: String(d.name ?? (0, desktops_1.desktopIdOf)(d)), desktopIndex: i }] : []);
499
+ const groups = this.config.store.groups ?? [];
500
+ const inherited = (p) => p.options?.account === undefined && groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account === id;
501
+ const profiles = (this.config.store.profiles ?? [])
502
+ .filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && (p.options?.account === id || inherited(p)))
503
+ .map((p) => ({ name: String(p.name ?? p.options?.host ?? ''), profileId: p.id ? String(p.id) : undefined }));
504
+ return [...desktops, ...profiles];
505
+ }
506
+ /**
507
+ * Adds a saved account (no id) or changes one. `password`: the new one for the keychain; undefined keeps what is
508
+ * there. A new user name or domain without a new password drops the old password: it was for the old account.
509
+ * Returns the account's id, and the keychain's complaint if the password couldn't be saved.
510
+ */
511
+ async saveAccount(account, password) {
512
+ const list = this.accounts();
513
+ const old = list.find(a => a.id === account.id);
514
+ const entry = {
515
+ id: old?.id ?? (0, accounts_1.newAccountId)(list),
516
+ name: account.name.trim() || (0, accounts_1.signInName)(account),
517
+ username: account.username.trim(),
518
+ ...account.domain?.trim() ? { domain: account.domain.trim() } : {},
519
+ };
520
+ // The password first: a new user name must not be written while the old password (for the old name) stays, nor
521
+ // a new password reported as saved when the store refused it.
522
+ let keychainError;
523
+ if (password !== undefined && password !== '') {
524
+ 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); });
525
+ }
526
+ else if (old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
527
+ await (0, signin_1.forgetCredentialsOrFail)((0, accounts_1.accountKey)(entry.id)).catch(e => { keychainError = `the old password couldn't be removed: ${e?.message ?? e}`; });
528
+ }
529
+ if (keychainError && !old) {
530
+ // Not added at all: a retry would otherwise make a second account.
531
+ return { id: entry.id, keychainError: `${keychainError}. The account wasn't added` };
532
+ }
533
+ if (keychainError && old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
534
+ // The sign-in name stays as it was; the account's name can still change.
535
+ entry.username = old.username;
536
+ if (old.domain) {
537
+ entry.domain = old.domain;
538
+ }
539
+ else {
540
+ delete entry.domain;
541
+ }
542
+ }
543
+ this.config.store.remoteDesktop.accounts = old ? list.map(a => a === old ? entry : a) : [...list, entry];
544
+ await this.config.save();
545
+ this.changed$.next();
546
+ return { id: entry.id, keychainError };
547
+ }
548
+ /** A desktop form's "New account…": saves it and hands it back for the form's list. */
549
+ async addAccountFromForm(input) {
550
+ const { id, keychainError } = await this.saveAccount({ name: input.name, username: input.username, domain: input.domain }, input.password);
551
+ if (keychainError) {
552
+ throw new Error(keychainError);
553
+ }
554
+ return this.accounts().find(a => a.id === id);
555
+ }
556
+ /** Whether a password is saved for the account; 'unknown' while the Vault is locked (it isn't unlocked for this). */
557
+ accountHasPassword(id) {
558
+ return (0, signin_1.hasCredentials)((0, accounts_1.accountKey)(id));
559
+ }
560
+ /** "Remove…" on a saved account: asks first, naming the desktops that use it. Resolves true when removed. */
561
+ async confirmRemoveAccount(id) {
562
+ const account = this.accounts().find(a => a.id === id);
563
+ if (!account) {
564
+ return false;
565
+ }
566
+ const uses = this.accountUses(id);
567
+ const { response } = await this.platform.showMessageBox({
568
+ type: 'warning',
569
+ message: `Remove the account "${account.name}"?`,
570
+ detail: `${(0, accounts_1.signInName)(account)}. Its saved password is removed too. ` + (uses.length
571
+ ? `${uses.length === 1 ? 'One desktop uses' : `${uses.length} desktops use`} it and will ask for an account when connecting: ${uses.map(u => u.name).join(', ')}.`
572
+ : 'No desktop uses it.'),
573
+ buttons: ['Remove', 'Keep'],
574
+ defaultId: 1,
575
+ cancelId: 1,
576
+ });
577
+ if (response !== 0) {
578
+ return false;
579
+ }
580
+ await this.removeAccount(id);
581
+ return true;
582
+ }
583
+ /** Removes a saved account and its password; desktops that used it go back to asking. */
584
+ async removeAccount(id) {
585
+ const store = this.config.store.remoteDesktop;
586
+ store.accounts = this.accounts().filter(a => a.id !== id);
587
+ if (this.configuredDesktops().some(d => d.account === id)) {
588
+ store.desktops = this.configuredDesktops().map(d => {
589
+ if (d.account !== id) {
590
+ return d;
591
+ }
592
+ const { account: _account, ...rest } = d;
593
+ return rest;
594
+ });
595
+ }
596
+ for (const profile of this.config.store.profiles ?? []) {
597
+ if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.account === id) {
598
+ profile.options.account = '';
599
+ }
600
+ }
601
+ // A profile group's default for remote desktop profiles, too.
602
+ for (const group of this.config.store.groups ?? []) {
603
+ const options = group?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options;
604
+ if (options?.account === id) {
605
+ options.account = '';
606
+ }
607
+ }
608
+ await this.config.save();
609
+ this.changed$.next();
610
+ 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}`));
611
+ }
483
612
  /** `remoteDesktop.desktops`: the desktops configured behind SSH hosts. */
484
613
  configuredDesktops() {
485
614
  const list = this.config.store.remoteDesktop?.desktops;
@@ -500,6 +629,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
500
629
  const entry = await (0, desktopForm_1.askDesktop)(pane.element.nativeElement, {
501
630
  title: `Add a desktop reached through ${target.label}`,
502
631
  action: 'Add and open',
632
+ accounts: this.accounts(),
633
+ addAccount: input => this.addAccountFromForm(input),
503
634
  entry: { via: hostname, host: '127.0.0.1', port: 3389 },
504
635
  check: e => this.addressTaken(e, -1),
505
636
  });
@@ -524,20 +655,37 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
524
655
  * sharpness along; a new user name drops the saved account, which was for the old one. Open sessions keep running.
525
656
  */
526
657
  async editDesktop(pane, index) {
527
- const old = this.configuredDesktops()[index];
528
- if (!old) {
658
+ if (!this.configuredDesktops()[index]) {
529
659
  return;
530
660
  }
531
661
  if (this.isVisible(pane)) {
532
662
  this.showConsole(pane);
533
663
  }
534
- const entry = await (0, desktopForm_1.askDesktop)(pane.element.nativeElement, {
535
- title: `Edit ${old.name ?? (0, desktops_1.desktopIdOf)(old)} (behind ${old.via})`,
664
+ await this.editDesktopIn(pane.element.nativeElement, index);
665
+ pane.frontend?.focus();
666
+ }
667
+ /** What a desktop's `via` can name: the SSH profiles' names and hosts (see viaMatches), for the form to suggest. */
668
+ async sshHosts() {
669
+ const profiles = await this.profiles.getProfiles().catch(() => []);
670
+ const names = profiles.filter((p) => p.type === 'ssh' && !p.isTemplate)
671
+ .flatMap((p) => [p.name?.replace(/\s*\(\.ssh\/config\)$/, ''), p.options?.host].filter(Boolean));
672
+ return [...new Set(names)].sort((a, b) => a.localeCompare(b));
673
+ }
674
+ /** "Edit a desktop" with the form over any element (a pane, or the settings page). */
675
+ async editDesktopIn(host, index, askVia = false) {
676
+ const old = this.configuredDesktops()[index];
677
+ if (!old) {
678
+ return;
679
+ }
680
+ const entry = await (0, desktopForm_1.askDesktop)(host, {
681
+ title: `Edit ${old.name ?? (0, desktops_1.desktopIdOf)(old)}${askVia ? '' : ` (behind ${old.via})`}`,
536
682
  action: 'Save',
683
+ accounts: this.accounts(),
684
+ addAccount: input => this.addAccountFromForm(input),
685
+ hosts: askVia ? await this.sshHosts() : undefined,
537
686
  entry: old,
538
687
  check: e => this.addressTaken(e, index),
539
688
  });
540
- pane.frontend?.focus();
541
689
  // Compared with the entry as it is now: the config can change while the form shows.
542
690
  const list = this.configuredDesktops();
543
691
  if (!entry || list[index] !== old) {
@@ -547,7 +695,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
547
695
  this.config.store.remoteDesktop.desktops = list;
548
696
  this.config.save();
549
697
  this.changed$.next();
550
- const accountChanged = (old.username ?? '') !== (entry.username ?? '') || (old.domain ?? '') !== (entry.domain ?? '');
698
+ const accountChanged = (old.username ?? '') !== (entry.username ?? '') || (old.domain ?? '') !== (entry.domain ?? '') || (old.account ?? '') !== (entry.account ?? '');
551
699
  await this.desktopEdited((0, desktops_1.desktopIdOf)(old), (0, desktops_1.desktopIdOf)(entry), accountChanged, false);
552
700
  }
553
701
  /**
@@ -610,6 +758,21 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
610
758
  await this.importRdp(new Uint8Array(await file.arrayBuffer()), file.name);
611
759
  }
612
760
  }
761
+ /**
762
+ * The profile group for remote desktop profiles the plugin makes itself (an .rdp import, "New profile…" on the
763
+ * settings page): "Remote desktops", made the first time. Profiles made on Tabby's Profiles page go where the user
764
+ * puts them.
765
+ */
766
+ async remoteDesktopGroup() {
767
+ const groups = await this.profiles.getProfileGroups({ includeNonUserGroup: false });
768
+ const existing = groups.find(g => g.name === 'Remote desktops');
769
+ if (existing?.id) {
770
+ return existing.id;
771
+ }
772
+ const group = { name: 'Remote desktops', profiles: [] };
773
+ await this.profiles.newProfileGroup(group, { genId: true });
774
+ return group.id;
775
+ }
613
776
  /**
614
777
  * Adds a "Remote desktop (RDP)" profile from a .rdp file's contents (address, user name, domain; see rdpFile.ts)
615
778
  * and opens it; or opens the profile that already has that address and user. Returns the profile, or null.
@@ -631,6 +794,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
631
794
  type: targets_1.RDP_PROFILE_TYPE,
632
795
  name: fileName.replace(/^.*[\\/]/, '').replace(/\.rdp$/i, '') || parsed.host,
633
796
  icon: 'fas fa-desktop',
797
+ group: await this.remoteDesktopGroup(),
634
798
  options: { host: parsed.host, port: parsed.port, kind: 'windows', username: parsed.username ?? '', domain: parsed.domain ?? '', via: '' },
635
799
  };
636
800
  await this.profiles.newProfile(profile);
@@ -1437,23 +1601,43 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
1437
1601
  spec.port = endpoint.port;
1438
1602
  spec.username ??= endpoint.username;
1439
1603
  }
1440
- const saved = retryError ? null : await (0, signin_1.loadCredentials)(session.key);
1604
+ // A saved account (Settings): its user name, and its password from the keychain. One that has none yet, or that
1605
+ // this desktop just refused, is asked for here and saved for every desktop that uses the account.
1606
+ const account = spec.account ? this.accounts().find(a => a.id === spec.account) : undefined;
1607
+ if (spec.account && !account) {
1608
+ session.log.push('sign-in: its saved account no longer exists; asking');
1609
+ }
1610
+ const again = this.askAgain.delete(pane);
1611
+ // A desktop whose account is gone asks, rather than falling back to a login of its own saved earlier.
1612
+ let saved = retryError || again || spec.account && !account ? null : await (0, signin_1.loadCredentials)(account ? (0, accounts_1.accountKey)(account.id) : session.key);
1613
+ if (saved && account && saved.username !== (0, accounts_1.signInName)(account)) {
1614
+ // Kept for an earlier user name of the account (its change didn't reach the store): not this one's.
1615
+ session.log.push(`sign-in: the account's saved password is for ${saved.username}, not ${(0, accounts_1.signInName)(account)}; asking`);
1616
+ saved = null;
1617
+ }
1441
1618
  if (saved) {
1442
- return { host: spec.host, port: spec.port, credentials: saved, remember: false };
1619
+ if (account) {
1620
+ session.log.push(`sign-in: the saved account "${account.name}"`);
1621
+ }
1622
+ return { host: spec.host, port: spec.port, credentials: account ? { username: (0, accounts_1.signInName)(account), password: saved.password } : saved, remember: false, domain: account ? account.domain ?? '' : undefined };
1443
1623
  }
1444
1624
  session.status('');
1445
1625
  const asked = (0, signin_1.askCredentials)(session.overlay, {
1446
1626
  // The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via".
1447
1627
  title: spec.id === desktops_1.OWN_DESKTOP || target.direct || spec.kind === 'xrdp' && spec.host === '127.0.0.1' ? `Sign in to ${spec.name}` : `Sign in to ${spec.name} (via ${target.label})`,
1448
- username: spec.username,
1628
+ username: account ? (0, accounts_1.signInName)(account) : spec.username,
1449
1629
  error: retryError,
1450
1630
  canRemember: true,
1631
+ account: account?.name,
1451
1632
  }, session.disposed);
1452
1633
  if (session.visible) {
1453
1634
  setTimeout(() => session.visible && session.focusDesktop());
1454
1635
  }
1455
1636
  const entered = await asked;
1456
- return entered && { host: spec.host, port: spec.port, credentials: entered, remember: entered.remember };
1637
+ return entered && {
1638
+ host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, domain: account ? account.domain ?? '' : undefined,
1639
+ saveKey: account && (0, accounts_1.accountKey)(account.id), saveLabel: account && `saved account ${account.name}`, saveFor: account && (0, accounts_1.signInName)(account),
1640
+ };
1457
1641
  }
1458
1642
  async connect(pane, target, spec, session) {
1459
1643
  const alive = () => this.sessions.get(pane) === session;
@@ -1505,6 +1689,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
1505
1689
  return;
1506
1690
  }
1507
1691
  if (outcome.signInFailed && this.signsIn(spec) && attempt < 5 && alive()) {
1692
+ // The desktop's own saved account goes. A shared one stays until a new password is entered: this
1693
+ // desktop refusing it doesn't make it wrong for the others.
1508
1694
  await (0, signin_1.forgetCredentials)(session.key);
1509
1695
  retryError = outcome.signInFailed;
1510
1696
  endpoint = null;
@@ -1762,7 +1948,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
1762
1948
  const size = remoteSizeFor(session.overlay.getBoundingClientRect(), settings);
1763
1949
  const { width, height } = size;
1764
1950
  // DOMAIN\user or user@domain go to the server as typed; only the former needs splitting.
1765
- const [domain, username] = /^([^\\]+)\\(.+)$/.exec(credentials.username)?.slice(1) ?? [spec.domain ?? '', credentials.username];
1951
+ const [domain, username] = /^([^\\]+)\\(.+)$/.exec(credentials.username)?.slice(1) ?? [endpoint.domain ?? spec.domain ?? '', credentials.username];
1766
1952
  session.status(spec.id === desktops_1.OWN_DESKTOP ? `Connecting to ${target.label}…`
1767
1953
  : target.direct ? `Connecting to ${spec.name}…` : `Connecting to ${spec.name} via ${target.label}…`);
1768
1954
  const config = session.ui.configBuilder()
@@ -1847,8 +2033,14 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
1847
2033
  this.syncIndicator(session);
1848
2034
  this.changed$.next();
1849
2035
  this.reconnects.delete(pane); // connected again: a later drop starts over
1850
- if (endpoint.remember) {
1851
- (0, signin_1.saveCredentials)(session.key, credentials).then(() => session.log.push('sign-in: saved in the keychain'), e => session.log.push(`sign-in: keychain: ${e?.message ?? e}`));
2036
+ // Under a saved account only while that account is still there with the same sign-in name: the prompt may
2037
+ // have been open while it was removed or changed in Settings.
2038
+ const accountNow = endpoint.saveKey ? this.accounts().find(a => (0, accounts_1.accountKey)(a.id) === endpoint.saveKey) : undefined;
2039
+ if (endpoint.remember && endpoint.saveKey && (!accountNow || (0, accounts_1.signInName)(accountNow) !== endpoint.saveFor)) {
2040
+ session.log.push(`sign-in: not saved: the account ${accountNow ? 'changed' : 'was removed'} meanwhile`);
2041
+ }
2042
+ else if (endpoint.remember) {
2043
+ (0, signin_1.saveCredentials)(endpoint.saveKey ?? session.key, credentials, endpoint.saveLabel).then(() => session.log.push(`sign-in: saved ${endpoint.saveKey ? 'as the account\'s password ' : ''}in ${(0, signin_1.storeName)()}`), e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
1852
2044
  }
1853
2045
  if (session.visible) {
1854
2046
  session.focusDesktop();
@@ -1901,7 +2093,38 @@ function installStyle() {
1901
2093
  }
1902
2094
  styleInstalled = true;
1903
2095
  const style = document.createElement('style');
1904
- style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE;
2096
+ style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE + DRAGBAR_STYLE + accountForm_1.STYLE;
1905
2097
  document.head.appendChild(style);
2098
+ keepWindowDraggable();
2099
+ }
2100
+ /** A strip the height of the tab bar that stays a window drag region over a dialog (the plugin's, and Tabby's). */
2101
+ const DRAGBAR_STYLE = `
2102
+ .trd-form-dragbar { display: none; }
2103
+ .modal > .trd-form-dragbar { display: block; position: fixed; left: 0; right: 0; top: 0; height: var(--tabs-height, 38px); -webkit-app-region: drag; }
2104
+ `;
2105
+ /**
2106
+ * Tabby's dialogs (ng-bootstrap modals) cover the whole window, tab bar included, with nothing marked as a drag
2107
+ * region, so the window can't be moved while one shows. Each gets a strip where the tab bar is; the dialog box itself
2108
+ * is below it. The plugin's own dialogs do the same (see settingsPage.ts).
2109
+ */
2110
+ function keepWindowDraggable() {
2111
+ const strip = (modal) => {
2112
+ if (!modal.querySelector(':scope > .trd-form-dragbar')) {
2113
+ const bar = document.createElement('div');
2114
+ bar.className = 'trd-form-dragbar';
2115
+ modal.prepend(bar);
2116
+ }
2117
+ };
2118
+ // ng-bootstrap appends each dialog (ngb-modal-window.modal) to the body itself: only the body's children are watched.
2119
+ document.querySelectorAll('body > .modal').forEach(strip);
2120
+ new MutationObserver(records => {
2121
+ for (const record of records) {
2122
+ record.addedNodes.forEach(node => {
2123
+ if (node instanceof Element && node.classList.contains('modal')) {
2124
+ strip(node);
2125
+ }
2126
+ });
2127
+ }
2128
+ }).observe(document.body, { childList: true });
1906
2129
  }
1907
2130
  //# sourceMappingURL=desktop.service.js.map