tabby-rdp 0.5.0 → 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +216 -58
  3. package/TROUBLESHOOTING.md +77 -0
  4. package/dist/accountForm.js +0 -1
  5. package/dist/accountRevisions.js +46 -0
  6. package/dist/accounts.js +2 -2
  7. package/dist/audio.js +0 -1
  8. package/dist/authorities.js +331 -0
  9. package/dist/byteQueue.js +102 -0
  10. package/dist/clipboard.js +106 -0
  11. package/dist/connectionStatus.js +0 -1
  12. package/dist/desk.js +16 -47
  13. package/dist/deskScript.js +196 -6
  14. package/dist/desktop.service.js +1499 -269
  15. package/dist/desktopForm.js +53 -18
  16. package/dist/desktops.js +232 -24
  17. package/dist/diskStorage.js +212 -0
  18. package/dist/drives.js +271 -30
  19. package/dist/fileTransfer.js +960 -132
  20. package/dist/gateway.js +216 -57
  21. package/dist/header.js +76 -5
  22. package/dist/help.js +16 -47
  23. package/dist/hostCompat.js +86 -0
  24. package/dist/hyperv.js +65 -15
  25. package/dist/index.js +7 -4
  26. package/dist/keyboard.js +0 -1
  27. package/dist/microphone.js +57 -13
  28. package/dist/ntlm.js +0 -1
  29. package/dist/osd.js +56 -15
  30. package/dist/packageChangeDialog.js +186 -0
  31. package/dist/probe.js +15 -3
  32. package/dist/rdcleanpath.js +319 -44
  33. package/dist/rdpFile.js +22 -7
  34. package/dist/rdpProfile.js +39 -21
  35. package/dist/releaseSettings.js +72 -0
  36. package/dist/releases.js +408 -0
  37. package/dist/remoteSetup.js +103 -23
  38. package/dist/sessionLog.js +53 -0
  39. package/dist/settingsPage.js +148 -122
  40. package/dist/signin.js +478 -48
  41. package/dist/ssh.js +99 -18
  42. package/dist/sshChannelStream.js +40 -6
  43. package/dist/targets.js +194 -40
  44. package/dist/ui.js +88 -20
  45. package/dist/unshowable.js +41 -0
  46. package/dist/updates.js +79 -56
  47. package/dist/vms.js +17 -3
  48. package/dist/wake.js +15 -8
  49. package/docs/desk.md +157 -0
  50. package/package.json +14 -4
  51. package/remote/trd-pty.py +74 -13
  52. package/vendor/SHA256SUMS +26 -0
  53. package/vendor/ironrdp_web_bg.wasm +0 -0
  54. package/dist/accountForm.js.map +0 -1
  55. package/dist/accounts.js.map +0 -1
  56. package/dist/audio.js.map +0 -1
  57. package/dist/connectionStatus.js.map +0 -1
  58. package/dist/desk.js.map +0 -1
  59. package/dist/deskScript.js.map +0 -1
  60. package/dist/desktop.service.js.map +0 -1
  61. package/dist/desktopForm.js.map +0 -1
  62. package/dist/desktops.js.map +0 -1
  63. package/dist/drives.js.map +0 -1
  64. package/dist/fileTransfer.js.map +0 -1
  65. package/dist/gateway.js.map +0 -1
  66. package/dist/header.js.map +0 -1
  67. package/dist/help.js.map +0 -1
  68. package/dist/hyperv.js.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/keyboard.js.map +0 -1
  71. package/dist/microphone.js.map +0 -1
  72. package/dist/ntlm.js.map +0 -1
  73. package/dist/osd.js.map +0 -1
  74. package/dist/probe.js.map +0 -1
  75. package/dist/rdcleanpath.js.map +0 -1
  76. package/dist/rdpFile.js.map +0 -1
  77. package/dist/rdpProfile.js.map +0 -1
  78. package/dist/remoteSetup.js.map +0 -1
  79. package/dist/settingsPage.js.map +0 -1
  80. package/dist/signin.js.map +0 -1
  81. package/dist/ssh.js.map +0 -1
  82. package/dist/sshChannelStream.js.map +0 -1
  83. package/dist/targets.js.map +0 -1
  84. package/dist/ui.js.map +0 -1
  85. package/dist/updates.js.map +0 -1
  86. package/dist/vms.js.map +0 -1
  87. package/dist/wake.js.map +0 -1
@@ -15,13 +15,17 @@ const path_1 = require("path");
15
15
  const tabby_core_1 = require("tabby-core");
16
16
  const tabby_settings_1 = require("tabby-settings");
17
17
  const accounts_1 = require("./accounts");
18
+ const clipboard_1 = require("./clipboard");
18
19
  const desktop_service_1 = require("./desktop.service");
19
20
  const desktops_1 = require("./desktops");
20
21
  const drives_1 = require("./drives");
21
22
  const osd_1 = require("./osd");
22
23
  const help_1 = require("./help");
23
24
  const updates_1 = require("./updates");
25
+ const releaseSettings_1 = require("./releaseSettings");
24
26
  const targets_1 = require("./targets");
27
+ const unshowable_1 = require("./unshowable");
28
+ const hostCompat_1 = require("./hostCompat");
25
29
  const REPO = 'https://github.com/meanaverage/tabby-rdp';
26
30
  const k = (s) => `<kbd>${(0, help_1.esc)(s)}</kbd>`;
27
31
  /** The page's tabs (as Tabby's "Profiles & connections" page has Profiles and Advanced), each holding sections by topic. */
@@ -31,7 +35,7 @@ const TABS = [
31
35
  { id: 'osd', title: 'Overlay', topics: ['osd'] },
32
36
  { id: 'desktops', title: 'Desktops', topics: ['desktops', 'certificates'] },
33
37
  { id: 'accounts', title: 'Accounts', topics: ['accounts'] },
34
- { id: 'troubleshooting', title: 'Troubleshooting', topics: ['troubleshooting'] },
38
+ { id: 'updates', title: 'Updates', topics: ['updates'] },
35
39
  ];
36
40
  /** The tab shown last, so that the page comes back to it. */
37
41
  let lastTab = 'start';
@@ -41,13 +45,22 @@ const STYLE = `
41
45
  .trd-settings { display: block; }
42
46
  .trd-settings h3 { margin-bottom: 4px; }
43
47
  .trd-settings .trd-lead { margin-bottom: 18px; opacity: 0.75; }
44
- .trd-settings .trd-links { white-space: nowrap; }
45
- .trd-settings .trd-lead a { margin-left: 10px; }
48
+ .trd-settings .trd-links { display: inline-flex; flex-wrap: wrap; gap: 6px 10px; margin-left: 10px; }
49
+ .trd-settings .trd-lead a { white-space: nowrap; }
46
50
  .trd-settings section .trd-lead a { margin-left: 0; cursor: pointer; text-decoration: underline; }
47
- .trd-settings .trd-update { display: flex; gap: 10px; align-items: center; margin-bottom: 18px; padding: 10px 12px; border-radius: 6px;
51
+ .trd-settings .trd-update { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 18px; padding: 10px 12px; border-radius: 6px;
48
52
  border: 1px solid rgba(80, 150, 255, 0.45); background: rgba(80, 150, 255, 0.1); }
49
53
  .trd-settings .trd-update > div { flex: auto; }
50
54
  .trd-settings .trd-update:empty { display: none; }
55
+ .trd-settings .trd-release-summary { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
56
+ .trd-settings .trd-release-summary h4 { margin: 3px 0; }
57
+ .trd-settings .trd-release-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0; }
58
+ .trd-settings .trd-release-actions select { width: auto; max-width: 100%; flex: 1 1 240px; }
59
+ .trd-settings .trd-release-notice { padding: 12px; border: 1px solid rgba(80,150,255,.45); border-radius: 6px; margin-bottom: 18px; }
60
+ .trd-settings .trd-release-error { color: var(--bs-danger, #e88); }
61
+ .trd-settings [data-release-settings] .trd-sub { font-size: 12px; opacity: .75; }
62
+ .trd-settings .trd-release-pause { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
63
+ @media (max-width: 700px) { .trd-settings [data-release-settings] .form-line { flex-wrap: wrap; gap: 8px; } }
51
64
  .trd-settings .nav-tabs { margin-bottom: 18px; }
52
65
  .trd-settings .nav-tabs .nav-link { cursor: pointer; }
53
66
  .trd-settings section { margin-bottom: 28px; scroll-margin-top: 12px; }
@@ -59,7 +72,7 @@ const STYLE = `
59
72
  .trd-settings .trd-card { display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: 6px;
60
73
  border: 1px solid rgba(128, 128, 128, 0.25); }
61
74
  .trd-settings .trd-card .trd-card-title { font-weight: 600; display: flex; align-items: center; gap: 6px; }
62
- .trd-settings h4 .trd-info, .trd-settings .trd-lead .trd-info { margin-left: 4px; vertical-align: middle; font-size: 13px; }
75
+ .trd-settings .trd-card-text .trd-info, .trd-settings .trd-lead .trd-info, .trd-settings .description .trd-info { margin-left: 4px; vertical-align: middle; font-size: 13px; }
63
76
  .trd-settings .trd-info { position: relative; display: inline-flex; opacity: 0.55; cursor: help; outline: none; font-weight: normal; }
64
77
  .trd-settings .trd-info:hover, .trd-settings .trd-info:focus { opacity: 1; }
65
78
  .trd-settings .trd-info::after { content: attr(data-tip); position: absolute; left: 0; top: calc(100% + 6px); z-index: 20; width: 260px;
@@ -148,10 +161,10 @@ exports.RemoteDesktopSettingsTab = RemoteDesktopSettingsTab = __decorate([
148
161
  ], RemoteDesktopSettingsTab);
149
162
  /**
150
163
  * The plugin's settings page: getting started, the settings, keys, desktops and certificates it keeps, and
151
- * troubleshooting. Plain DOM like the rest of the plugin; the lists follow config changes made elsewhere.
164
+ * updates. Plain DOM like the rest of the plugin; the lists follow config changes made elsewhere.
152
165
  */
153
166
  let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
154
- constructor(element, desktop, help, config, platform, injector, zone, hotkeys) {
167
+ constructor(element, desktop, help, config, platform, injector, zone, hotkeys, compat) {
155
168
  this.element = element;
156
169
  this.desktop = desktop;
157
170
  this.help = help;
@@ -160,6 +173,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
160
173
  this.injector = injector;
161
174
  this.zone = zone;
162
175
  this.hotkeys = hotkeys;
176
+ this.compat = compat;
163
177
  this.contentBox = true;
164
178
  this.trdSettings = true;
165
179
  this.subscriptions = [];
@@ -174,8 +188,6 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
174
188
  document.head.appendChild(style);
175
189
  }
176
190
  this.render();
177
- // A known newer version may be stale by now (the page is where it shows): ask npm again.
178
- this.injector.get(updates_1.UpdateCheck).refresh();
179
191
  // The preview is a desktop of the chosen resolution, scaled down to fit the page.
180
192
  const frame = this.root.querySelector('.trd-osd-preview');
181
193
  const scale = new ResizeObserver(() => this.scalePreview());
@@ -196,38 +208,33 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
196
208
  }
197
209
  render() {
198
210
  const mac = process.platform === 'darwin';
199
- const version = (() => {
200
- try {
201
- return require('../package.json').version;
202
- }
203
- catch {
204
- return '';
205
- }
206
- })();
211
+ const version = (0, updates_1.installedVersion)();
207
212
  this.root.innerHTML = `
208
213
  <h3>Remote Desktop</h3>
209
214
  <div class="trd-lead">Linux and Windows desktops in Tabby tabs, through SSH or directly.
210
215
  ${version ? `<span>tabby-rdp ${(0, help_1.esc)(version)}</span>` : ''}
211
- <span class="trd-links"><a href="#" data-link="${REPO}#readme">Guide</a><a href="#" data-link="${REPO}/issues">Report a problem</a><a href="#" data-link="${REPO}/discussions/categories/q-a">Ask a question</a></span></div>
212
- <div class="trd-update" data-update></div>
216
+ <span class="trd-links"><a href="#" data-link="${REPO}#readme">Guide</a><a href="#" data-link="${REPO}/issues">Report a problem</a><a href="#" data-link="${REPO}/discussions/categories/q-a">Ask a question</a><a href="#" data-link="${help_1.TROUBLESHOOTING_URL}">Troubleshooting</a></span></div>
213
217
  <ul class="nav nav-tabs" data-nav></ul>
214
218
 
215
219
  <section data-topic="start">
216
220
  <h4>Getting started</h4>
217
221
  <div class="trd-cards">
218
222
  <div class="trd-card">
219
- <div class="trd-card-title">Over SSH ${this.info('The host is set up on the first connection: GNOME, xrdp, or Windows with OpenSSH. Nothing to install here, no ports to open.')}</div>
220
- <div class="trd-card-text">In an SSH tab, press <span data-hotkey></span> or click <b>Desktop</b>.</div>
223
+ <div class="trd-card-title">Over SSH</div>
224
+ <div class="trd-card-text">In an SSH tab, press <span data-hotkey></span> or click <b>Desktop</b>.
225
+ ${this.info('The host is set up on the first connection: GNOME, xrdp, or Windows with OpenSSH. Nothing to install here, no ports to open.')}</div>
221
226
  <button class="btn btn-secondary btn-sm" data-action="ssh">Open a connection…</button>
222
227
  </div>
223
228
  <div class="trd-card">
224
- <div class="trd-card-title">On your network ${this.info('The desktop opens in a tab of its own. For a quick connection, type user@host in the profile selector.')}</div>
225
- <div class="trd-card-text">A profile that connects straight to an RDP server.</div>
229
+ <div class="trd-card-title">On your network</div>
230
+ <div class="trd-card-text">A profile that connects straight to an RDP server.
231
+ ${this.info('The desktop opens in a tab of its own. For a quick connection, type user@host in the profile selector.')}</div>
226
232
  <button class="btn btn-secondary btn-sm" data-action="profile">New profile…</button>
227
233
  </div>
228
234
  <div class="trd-card">
229
- <div class="trd-card-title">From an .rdp file ${this.info('A file from Remote Desktop Connection, or from your admin. Its address, user name and domain make the profile; the password is asked for.')}</div>
230
- <div class="trd-card-text">The file becomes a profile.</div>
235
+ <div class="trd-card-title">From an .rdp file</div>
236
+ <div class="trd-card-text">The file becomes a profile.
237
+ ${this.info('A file from Remote Desktop Connection, or from your admin. Its address, user name and domain make the profile; the password is asked for.')}</div>
231
238
  <button class="btn btn-secondary btn-sm" data-action="import">Import an .rdp file…</button>
232
239
  </div>
233
240
  </div>
@@ -239,8 +246,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
239
246
  <div class="form-line">
240
247
  <div class="header">
241
248
  <div class="title">When the pane is resized</div>
242
- <div class="description">Resizing follows splits and the window; a kept resolution can be scaled or
243
- shown at actual size with scroll bars.</div>
249
+ <div class="description">Fixed resolutions use scaling or scroll bars.</div>
244
250
  </div>
245
251
  <select class="form-control" data-setting="resize">
246
252
  <option value="live">Resize to fit</option>
@@ -252,49 +258,58 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
252
258
  <div class="form-line">
253
259
  <div class="header">
254
260
  <div class="title">Sharpness</div>
255
- <div class="description">Retina renders at your screen's pixels, with the remote's scale set to
256
- match. A desktop can have its own, in its tab's menu.</div>
261
+ <div class="description">Retina uses your screen's pixel density and matching remote scaling.</div>
257
262
  </div>
258
263
  <select class="form-control" data-setting="sharpness">
259
264
  <option value="standard">Standard</option>
260
265
  <option value="retina">Retina</option>
261
266
  </select>
262
267
  </div>
263
- ${this.toggleLine('h264', 'Video decoding (H.264)', 'Decodes what the remote sends as video, hardware-accelerated where available. Applies on the next connection.')}
264
- ${this.toggleLine('connectionStatus', 'Show connection status', 'A small line in the desktop\'s corner: throughput, frames per second, round trip, and how it\'s connected.')}
268
+ ${this.toggleLine('h264', 'Video decoding (H.264)', 'Hardware acceleration where available. Applies on the next connection.')}
269
+ ${this.toggleLine('connectionStatus', 'Show connection status', 'Show throughput, frame rate, latency and connection details.')}
265
270
 
266
271
  <h5>Sound</h5>
267
- ${this.toggleLine('sound', 'Sound', 'Play the remote desktop\'s sound here. Applies on the next connection.')}
268
- ${this.toggleLine('microphone', 'Microphone', 'Send your microphone while an app there records, with a red dot in the corner meanwhile. Applies on the next connection.')}
272
+ ${this.toggleLine('sound', 'Sound', 'Play remote audio. Applies on the next connection.')}
273
+ ${this.toggleLine('microphone', 'Microphone', `Share your microphone when a remote app requests it. Applies on the next connection; turning it off stops sharing immediately. ${this.info('A red indicator remains visible while sharing, including from background desktops. Its menu can stop sharing with an individual desktop. That desktop stays muted through reconnects until re-enabled or Tabby closes.')}`)}
274
+
275
+ <h5>Clipboard</h5>
276
+ <div class="form-line">
277
+ <div class="header">
278
+ <div class="title">Clipboard sharing</div>
279
+ <div class="description">Share text, images and files. Desktops can override this setting. Applies on reconnect;
280
+ restricting sharing stops local sends immediately.
281
+ ${this.info('Sharing can expose passwords and let the remote replace your local clipboard. Only the focused desktop receives your clipboard. Off disables text, images and clipboard file transfers. Restricting sharing also withdraws previously offered files.')}</div>
282
+ </div>
283
+ <select class="form-control" data-setting="clipboard">
284
+ ${Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, label]) => `<option value="${mode}">${(0, help_1.esc)(label)}</option>`).join('')}
285
+ </select>
286
+ </div>
269
287
 
270
288
  <h5>Shared folders</h5>
271
- <div class="trd-lead">Folders from this computer as drives on the remote desktop, under <code>\\\\tsclient</code> in
272
- Explorer. Applies on the next connection.
273
- ${this.info('Every remote desktop you connect to sees these folders, so share only what each of them may have. Read-only keeps the remote from changing anything in a folder. Windows mounts them (xrdp can, with FUSE); GNOME Remote Desktop doesn\'t serve drives.')}</div>
289
+ <div class="trd-lead">Available as remote drives (<code>\\\\tsclient</code> on Windows). Applies on the next connection.
290
+ ${this.info('Shared with every remote desktop. Use read-only to prevent remote changes. Supported by Windows and xrdp with FUSE; unavailable on GNOME Remote Desktop.')}</div>
274
291
  <div class="trd-list" data-list="folders"></div>
275
292
  <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="folder">Share a folder…</button></div>
276
293
 
277
294
  <h5>Keyboard</h5>
278
- ${this.toggleLine('unicodeKeys', 'Send text as typed', 'Sends the characters your keyboard produces rather than key positions, so dead keys (´ then e) and a layout the remote doesn\'t have come out right. Shortcuts with Ctrl, Alt or ⌘ still go by position. On GNOME, characters the remote\'s own layout lacks still can\'t be typed; Windows takes any. Input methods (Chinese, Japanese, Korean) aren\'t supported yet.')}
279
- ${mac ? this.toggleLine('macShortcuts', 'Mac-style shortcuts', 'Use ⌘ as Ctrl on the desktop, so ⌘C copies and ⌘V pastes. Tap ⌘ on its own for the Windows key. When off, ⌘ is always the Windows key.') : ''}
295
+ ${this.toggleLine('unicodeKeys', 'Send text as typed', `Send characters instead of physical keys. Modifier shortcuts still use key positions. ${this.info('Useful for dead keys or mismatched keyboard layouts. GNOME is limited to characters in its own layout. Chinese, Japanese and Korean input methods are not supported.')}`)}
296
+ ${mac ? this.toggleLine('macShortcuts', 'Mac-style shortcuts', '⌘C copies and ⌘V pastes. Tap ⌘ for the Windows key; when disabled, ⌘ always sends the Windows key.') : ''}
280
297
  <div class="form-line">
281
298
  <div class="header">
282
299
  <div class="title">Shortcuts</div>
283
- <div class="description">Switching between the desktop and the console, view only, screenshots and more: on the
284
- <a data-action="shortcuts">Getting started</a> tab, and in Tabby's Hotkeys.</div>
300
+ <div class="description">Manage in <a data-action="shortcuts">Getting started</a> or Tabby's Hotkeys.</div>
285
301
  </div>
286
302
  <div><span data-hotkey></span></div>
287
303
  </div>
288
304
 
289
305
  <h5>SSH hosts</h5>
290
- ${this.toggleLine('desk', 'desk: bring the console along', `Type <code>desk</code> in an SSH console to show that same shell on the desktop. Installs a small helper and a login line on GNOME hosts the next time a desktop opens there; off removes them.`)}
291
- ${this.toggleLine('discoverVMs', 'Find virtual machines on SSH hosts', 'Lists the host\'s VMs in its tab\'s menu, ready to open or start: libvirt VMs that have a desktop (RDP answering, or Windows and shut off), and on a Windows host, Hyper-V VMs (their consoles, through the host). Read-only: it runs virsh, or PowerShell\'s Get-VM, as you there, at most once a minute.')}
306
+ ${this.toggleLine('desk', 'desk integration', `Type <code>desk</code> to open your current SSH shell on the desktop. Installs a helper on GNOME hosts on the next desktop connection; disabling removes it.`)}
307
+ ${this.toggleLine('discoverVMs', 'Find virtual machines on SSH hosts', 'List libvirt and Hyper-V VMs in the SSH tab menu. Discovery is read-only and runs at most once a minute.')}
292
308
  <div class="form-line">
293
309
  <div class="header">
294
- <div class="title">Shut down VMs it started</div>
295
- <div class="description">A VM started to open its desktop goes back off after this long with no
296
- desktop open, also once Tabby is closed: a small watcher on the SSH host sees to it. VMs that
297
- were already running are left alone.</div>
310
+ <div class="title">Shut down started VMs</div>
311
+ <div class="description">Shut down VMs started by this plugin after the selected time with no desktop open.
312
+ Continues after Tabby closes; previously running VMs are unaffected.</div>
298
313
  </div>
299
314
  <select class="form-control" data-setting="shutDownIdle">
300
315
  <option value="0">Never</option>
@@ -304,16 +319,13 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
304
319
  </select>
305
320
  </div>
306
321
 
307
- <h5>Updates</h5>
308
- ${this.toggleLine('checkUpdates', 'Tell me about new versions', 'Once a day, asks npm (registry.npmjs.org) for the latest tabby-rdp, and says so here and in the menus when there is one. Nothing is sent but the request. Tabby itself shows plugin upgrades only on its Plugins page.')}
309
322
  </section>
310
323
 
311
324
  <section data-topic="osd">
312
325
  <h4>Desktop name overlay</h4>
313
- <div class="trd-lead">Names the desktop for a moment when it connects, when you switch to it, and when you click
314
- into its pane, like a TV naming its input.</div>
326
+ <div class="trd-lead">Show the desktop name when connecting, switching desktops or focusing a pane.</div>
315
327
  <div class="trd-osd-preview"><div class="trd-osd-stage"><div class="trd-osd"></div></div>
316
- <select class="form-control form-control-sm trd-osd-resolution" data-preview-resolution title="The desktop resolution the preview stands for: the overlay's size on screen depends on it">
328
+ <select class="form-control form-control-sm trd-osd-resolution" data-preview-resolution title="Preview resolution">
317
329
  <option value="1280x720">1280 × 720</option>
318
330
  <option value="1920x1080">1920 × 1080</option>
319
331
  <option value="2560x1440">2560 × 1440</option>
@@ -321,10 +333,9 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
321
333
  </select></div>
322
334
  <div class="form-line">
323
335
  <div class="header"><div class="title">Show</div>
324
- <div class="description">"When it helps": in split panes, and for a desktop other than the tab's own
325
- connection, where the tab's title doesn't say which it is.</div></div>
336
+ <div class="description">Automatic shows names in split panes and when the tab title identifies a different connection.</div></div>
326
337
  <select class="form-control" data-osd="show">
327
- <option value="auto">When it helps</option>
338
+ <option value="auto">Automatic</option>
328
339
  <option value="always">Every time</option>
329
340
  <option value="off">Never</option>
330
341
  </select>
@@ -334,7 +345,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
334
345
  <select class="form-control" data-osd="font">${Object.entries(osd_1.OSD_FONTS).map(([id, f]) => `<option value="${id}">${(0, help_1.esc)(f.label)}</option>`).join('')}</select>
335
346
  </div>
336
347
  <div class="form-line">
337
- <div class="header"><div class="title">Size</div><div class="description">Smaller in a narrow pane, so a name fits.</div></div>
348
+ <div class="header"><div class="title">Size</div></div>
338
349
  <select class="form-control" data-osd="size">${Object.keys(osd_1.OSD_SIZES).map(id => `<option value="${id}">${id === 'huge' ? 'Extra large' : id[0].toUpperCase() + id.slice(1)}</option>`).join('')}</select>
339
350
  </div>
340
351
  <div class="form-line">
@@ -342,12 +353,11 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
342
353
  <select class="form-control" data-osd="position">${osd_1.OSD_POSITIONS.map(p => `<option value="${p}">${p === 'middle' ? 'Middle' : p.replace('-', ' ').replace(/^./, c => c.toUpperCase())}</option>`).join('')}</select>
343
354
  </div>
344
355
  <div class="form-line">
345
- <div class="header"><div class="title">Color</div><div class="description">White reads on any picture, with the soft
346
- shadow behind it. The line before the second row takes your theme's accent color.</div></div>
356
+ <div class="header"><div class="title">Color</div></div>
347
357
  <div class="trd-color"><input type="color" data-osd="color"><button class="btn btn-link btn-sm" data-action="osd-white">White</button></div>
348
358
  </div>
349
359
  <div class="form-line">
350
- <div class="header"><div class="title">How long</div></div>
360
+ <div class="header"><div class="title">Duration</div></div>
351
361
  <select class="form-control" data-osd="seconds">
352
362
  <option value="1.5">1.5 seconds</option>
353
363
  <option value="2.5">2.5 seconds</option>
@@ -355,12 +365,13 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
355
365
  <option value="6">6 seconds</option>
356
366
  </select>
357
367
  </div>
358
- <button class="btn btn-secondary btn-sm" data-action="osd-try">Show it on open desktops</button>
368
+ <button class="btn btn-secondary btn-sm" data-action="osd-try">Preview on open desktops</button>
359
369
  </section>
360
370
 
361
371
  <section data-topic="keyboard">
362
- <h4>Shortcuts ${this.info('These work while the desktop has the keyboard. Tabby\'s other shortcuts go to the desktop then, except switching tabs and full screen.')}</h4>
363
- <div class="trd-lead">Also listed in Tabby's <a data-action="hotkeys">Hotkeys</a>.</div>
372
+ <h4>Shortcuts</h4>
373
+ <div class="trd-lead">Also listed in Tabby's <a data-action="hotkeys">Hotkeys</a>.
374
+ ${this.info('These work while the desktop has the keyboard. Tabby\'s other shortcuts go to the desktop then, except switching tabs and full screen.')}</div>
364
375
  <div class="trd-hotkeys" data-list="hotkeys"></div>
365
376
  <h4 style="margin-top: 24px">Keys and tips</h4>
366
377
  <table class="trd-keys"><tbody>
@@ -378,15 +389,17 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
378
389
  <section data-topic="desktops">
379
390
  <h4>Desktops</h4>
380
391
  <div class="trd-lead">Remote desktop profiles, and desktops added from SSH tabs.
381
- ${this.info('A desktop added from an SSH tab\'s menu belongs to that host. That is for hosts without a profile, such as an ssh typed in a terminal.')}</div>
392
+ ${this.info('A desktop added from an SSH tab\'s menu belongs to that host. That is for hosts without a profile, such as ssh run in a local terminal.')}</div>
382
393
  <div class="trd-list" data-list="desktops"></div>
383
- <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="desktop">Add a desktop…</button></div>
394
+ <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="desktop">Add desktop…</button></div>
395
+ <h4 style="margin-top: 24px">Open desktops</h4>
396
+ <div class="trd-list" data-list="sessions"></div>
384
397
  </section>
385
398
 
386
399
  <section data-topic="accounts">
387
400
  <h4>Accounts</h4>
388
- <div class="trd-lead">One sign-in for several desktops: when its password changes, change it once here.
389
- ${this.info('A desktop picks its account in its profile\'s settings, or in the form that adds a desktop behind an SSH host. A profile group\'s defaults can pick one for all of its profiles.')}</div>
401
+ <div class="trd-lead">Reusable sign-in details for multiple desktops.
402
+ ${this.info('Choose an account in a desktop\'s settings or a profile group\'s defaults.')}</div>
390
403
  <div class="trd-list" data-list="accounts"></div>
391
404
  <div class="trd-add">
392
405
  <button class="btn btn-secondary btn-sm" data-action="account">Add an account…</button>
@@ -399,14 +412,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
399
412
  <div class="trd-list" data-list="certificates"></div>
400
413
  </section>
401
414
 
402
- <section data-topic="troubleshooting">
403
- <h4>Troubleshooting</h4>
404
- <div data-list="troubleshooting">${help_1.TROUBLESHOOTING.map(e => `
405
- <details data-entry="${(0, help_1.esc)(e.id)}"><summary>${(0, help_1.esc)(e.title)}</summary><div>${e.body}</div></details>`).join('')}
406
- </div>
407
- <h4 style="margin-top: 18px">Open desktops</h4>
408
- <div class="trd-list" data-list="sessions"></div>
409
- </section>`;
415
+ <section data-topic="updates"><h4>Updates</h4><div class="trd-update" data-update></div><div data-release-settings></div></section>`;
410
416
  // The sections go into the tabs' bodies; one tab shows at a time.
411
417
  const nav = this.root.querySelector('[data-nav]');
412
418
  for (const tab of TABS) {
@@ -467,7 +473,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
467
473
  'osd-white': () => this.changeOsd({ color: '' }),
468
474
  'osd-try': () => {
469
475
  if (!this.desktop.showOsdEverywhere()) {
470
- this.injector.get(tabby_core_1.NotificationsService).info('No desktop is showing right now');
476
+ this.injector.get(tabby_core_1.NotificationsService).info('No visible desktops.');
471
477
  }
472
478
  },
473
479
  };
@@ -476,7 +482,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
476
482
  }));
477
483
  this.refresh();
478
484
  }
479
- /** An ⓘ that shows `text` while hovered or focused: detail that the card itself needn't carry. */
485
+ /** An ⓘ placed after a description, showing `text` while hovered or focused. */
480
486
  info(text) {
481
487
  return `<span class="trd-info" tabindex="0" role="note" data-tip="${(0, help_1.esc)(text)}"><i class="fas fa-info-circle"></i></span>`;
482
488
  }
@@ -530,7 +536,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
530
536
  el.value = String(settings[key]);
531
537
  }
532
538
  });
533
- // A newer version: say so at the top, with where to get it.
539
+ // Update notices and controls belong to the Updates page.
534
540
  const updates = this.injector.get(updates_1.UpdateCheck);
535
541
  const banner = this.root.querySelector('[data-update]');
536
542
  if (banner) {
@@ -539,6 +545,10 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
539
545
  banner.querySelector('[data-upgrade]')?.addEventListener('click', () => updates.upgrade());
540
546
  banner.querySelector('[data-whatsnew]')?.addEventListener('click', () => updates.whatsNew());
541
547
  }
548
+ (0, releaseSettings_1.renderReleaseSettings)(this.root.querySelector('[data-release-settings]'), updates, {
549
+ notes: version => updates.whatsNew(version), plugins: () => this.help.openSettings('plugins'),
550
+ link: url => this.platform.openExternal(url),
551
+ });
542
552
  const osd = settings.osd;
543
553
  this.root.querySelectorAll('[data-osd]').forEach(el => {
544
554
  const key = el.dataset.osd;
@@ -589,41 +599,51 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
589
599
  // Remote desktop profiles first (Tabby's editor), then the desktops added from SSH tabs (the plugin's form).
590
600
  const profiles = (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && p.options?.host);
591
601
  const groups = this.config.store.groups ?? [];
602
+ // Every field as text whatever the config holds (see configText): one that can't be shown would leave this list,
603
+ // and the lists after it on the page, unbuilt.
592
604
  const profileRows = profiles.map(p => {
593
605
  const o = p.options;
594
- const address = `${o.host}:${o.port || 3389}`;
595
- const via = o.via ? (this.config.store.profiles ?? []).find((x) => x.id === o.via)?.name ?? 'an SSH profile' : null;
606
+ const address = `${(0, desktops_1.configText)(o.host)}:${(0, desktops_1.configText)(o.port || 3389)}`;
607
+ const via = o.via ? (0, desktops_1.configText)((this.config.store.profiles ?? []).find((x) => x.id === o.via)?.name) || 'an SSH profile' : null;
596
608
  const inherited = o.account === undefined ? groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account : undefined;
597
609
  const account = accountOf(o.account || inherited);
610
+ // Its own clipboard, or its group's or the type's default: what it connects with, as Tabby resolves the profile.
611
+ const clipboard = (0, clipboard_1.ownClipboard)(this.injector.get(tabby_core_1.ProfilesService).getConfigProxyForProfile(p).options?.clipboard);
612
+ const [username, kind, gateway] = [(0, desktops_1.configText)(o.username), (0, desktops_1.configText)(o.kind), (0, desktops_1.configText)(o.gateway)];
598
613
  const details = [
599
- account ? `Signs in as ${(0, help_1.esc)(account.name)}` : o.username ? `Signs in as ${(0, help_1.esc)(o.username)}` : '',
600
- o.kind && o.kind !== 'windows' ? (0, help_1.esc)(o.kind) : '',
601
- o.gateway ? `Through the gateway ${(0, help_1.esc)(o.gateway)}${accountOf(o.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(o.gatewayAccount).name)}` : ''}` : '',
614
+ account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
615
+ kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
616
+ gateway ? `Through the gateway ${(0, help_1.esc)(gateway)}${accountOf(o.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(o.gatewayAccount).name)}` : ''}` : '',
617
+ clipboard ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[clipboard].toLowerCase())}` : '',
602
618
  ].filter(Boolean).join('<br>');
603
- return this.row((0, help_1.esc)(p.name ?? address), `${(0, help_1.esc)(address)}${via ? ` via ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
619
+ return this.row((0, help_1.esc)((0, desktops_1.configText)(p.name) || address), `${(0, help_1.esc)(address)}${via ? ` via ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
604
620
  { label: 'Edit…', run: () => { this.editProfile(p.id); } },
605
621
  { label: 'Remove…', danger: true, run: () => { this.removeProfile(p.id); } },
606
622
  ]);
607
623
  });
608
624
  const desktops = this.desktop.configuredDesktops();
609
- const desktopRows = desktops.map((d, i) => {
625
+ // An item that is no entry describes no desktop, and isn't listed (see isDesktopEntry).
626
+ const desktopRows = desktops.flatMap((d, i) => {
627
+ if (!(0, desktops_1.isDesktopEntry)(d)) {
628
+ return [];
629
+ }
610
630
  // A Hyper-V VM has no address of its own: its console is its host's to give.
611
- const address = d.hyperv ? 'Hyper-V VM' : `${d.host ?? '127.0.0.1'}:${d.port ?? 3389}`;
631
+ const { name, address, via, hyperv } = (0, desktops_1.entryText)(d);
612
632
  const account = accountOf(d.account);
633
+ const [username, kind, gateway, vm, mac] = [(0, desktops_1.configText)(d.username), (0, desktops_1.configText)(d.kind), (0, desktops_1.configText)(d.gateway), (0, desktops_1.configText)(d.wake?.vm), (0, desktops_1.configText)(d.wake?.mac)];
613
634
  const details = [
614
- account ? `Signs in as ${(0, help_1.esc)(account.name)}` : d.username ? `Signs in as ${(0, help_1.esc)(d.username)}` : '',
615
- d.kind && d.kind !== 'windows' ? (0, help_1.esc)(d.kind) : '',
616
- d.gateway && !d.hyperv ? `Through the gateway ${(0, help_1.esc)(d.gateway)}${accountOf(d.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(d.gatewayAccount).name)}` : ''}` : '',
617
- d.wake?.vm ? `Starts VM ${(0, help_1.esc)(d.wake.vm)} when off` : d.wake?.mac ? `Wakes ${(0, help_1.esc)(d.wake.mac)} when off` : '',
635
+ account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
636
+ kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
637
+ gateway && !hyperv ? `Through the gateway ${(0, help_1.esc)(gateway)}${accountOf(d.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(d.gatewayAccount).name)}` : ''}` : '',
638
+ vm ? `Starts VM ${(0, help_1.esc)(vm)} when off` : mac ? `Wakes ${(0, help_1.esc)(mac)} when off` : '',
639
+ (0, clipboard_1.ownClipboard)(d.clipboard) ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[(0, clipboard_1.ownClipboard)(d.clipboard)].toLowerCase())}` : '',
618
640
  ].filter(Boolean).join('<br>');
619
- return this.row((0, help_1.esc)(d.name ?? address), `${(0, help_1.esc)(address)} ${d.hyperv ? 'on' : 'behind'} ${(0, help_1.esc)(d.via)}${details ? `<br>${details}` : ''}`, [
620
- { label: 'Edit…', run: () => { this.desktop.editDesktopIn(this.root, i, true); } },
621
- { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveDesktop(i); } },
622
- ]);
641
+ return [this.row((0, help_1.esc)(name), `${hyperv ? 'Hyper-V VM' : (0, help_1.esc)(address)}${via ? ` ${hyperv ? 'on' : 'behind'} ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
642
+ { label: 'Edit…', run: () => { this.desktop.editDesktopIn(this.root, i, true); } },
643
+ { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveDesktop(i); } },
644
+ ])];
623
645
  });
624
- list.replaceChildren(...profileRows, ...desktopRows, this.empty(profileRows.length + desktopRows.length
625
- ? 'Profiles are also in Profiles &amp; connections; a desktop behind a host also in that host\'s SSH tab: right-click › Settings.'
626
- : 'None yet. <b>Add a desktop…</b> makes a profile: an RDP server on your network, or one behind an SSH profile\'s host.'));
646
+ list.replaceChildren(...profileRows, ...desktopRows, ...(profileRows.length + desktopRows.length ? [] : [this.empty('No saved desktops.')]));
627
647
  }
628
648
  renderFolders() {
629
649
  const list = this.root.querySelector('[data-list=folders]');
@@ -645,7 +665,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
645
665
  }));
646
666
  row.insertBefore(check, row.querySelector('button'));
647
667
  return row;
648
- }), ...folders.length ? [] : [this.empty('None shared. The clipboard still carries files either way: drop them on the desktop, or copy them there and save them here.')]);
668
+ }), ...folders.length ? [] : [this.empty('No shared folders.')]);
649
669
  }
650
670
  /** Asks for a folder and shares it under its own name (numbered when another share has it). */
651
671
  async shareFolder() {
@@ -655,7 +675,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
655
675
  }
656
676
  const folders = this.desktop.sharedFolders();
657
677
  if (folders.some(f => f.path === picked)) {
658
- this.injector.get(tabby_core_1.NotificationsService).info('That folder is shared already');
678
+ this.injector.get(tabby_core_1.NotificationsService).info('This folder is already shared.');
659
679
  return;
660
680
  }
661
681
  this.desktop.setSharedFolders([...folders, { path: picked, name: (0, drives_1.driveName)((0, path_1.basename)(picked) || picked, folders), readOnly: false }]);
@@ -676,7 +696,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
676
696
  const used = uses.length
677
697
  ? `Used by ${uses.map(u => u.profileId ? `<a data-profile="${(0, help_1.esc)(u.profileId)}">${(0, help_1.esc)(u.name)}</a>`
678
698
  : u.desktopIndex !== undefined ? `<a data-desktop="${u.desktopIndex}">${(0, help_1.esc)(u.name)}</a>` : (0, help_1.esc)(u.name)).join(', ')}`
679
- : 'Not used by a desktop yet';
699
+ : 'Not used';
680
700
  const row = this.row((0, help_1.esc)(a.name), `<span>${(0, help_1.esc)((0, accounts_1.signInName)(a))}</span><br><span>${used}</span><span data-password></span>`, [
681
701
  { label: 'Edit…', run: () => this.editAccount(a) },
682
702
  { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveAccount(a.id); } },
@@ -687,11 +707,11 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
687
707
  checks = checks.then(() => row.isConnected ? this.desktop.accountHasPassword(a.id) : 'unknown').then(has => {
688
708
  const note = row.querySelector('[data-password]');
689
709
  if (note && has === 'no') {
690
- note.innerHTML = '<br>No password saved: asked for on the next connection';
710
+ note.innerHTML = '<br>No password saved';
691
711
  }
692
712
  }, () => null);
693
713
  return row;
694
- }), ...accounts.length ? [] : [this.empty('None yet. Without one, each desktop asks for its account and can remember it for itself.')]);
714
+ }), ...accounts.length ? [] : [this.empty('No saved accounts.')]);
695
715
  }
696
716
  /** The form under the list, for a new account or `account`. */
697
717
  editAccount(account) {
@@ -718,7 +738,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
718
738
  field('name').value = account?.name ?? '';
719
739
  field('username').value = account?.username ?? '';
720
740
  field('domain').value = account?.domain ?? '';
721
- field('password').placeholder = account ? 'Leave empty to keep the saved one' : 'Optional; asked on the first connection';
741
+ field('password').placeholder = account ? 'Leave empty to keep the saved password' : 'Optional; requested when connecting';
722
742
  form.querySelector('button[type=submit]').textContent = account ? 'Save' : 'Add';
723
743
  const close = () => {
724
744
  form.remove();
@@ -735,7 +755,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
735
755
  const username = field('username').value.trim();
736
756
  const name = field('name').value.trim();
737
757
  if (!username) {
738
- error.textContent = 'Give it a user name.';
758
+ error.textContent = 'Enter a user name.';
739
759
  field('username').focus();
740
760
  return;
741
761
  }
@@ -958,17 +978,21 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
958
978
  return;
959
979
  }
960
980
  const entries = this.desktop.trustedCertificates();
961
- list.replaceChildren(...entries.map(({ desktop, sha256 }) => {
981
+ list.replaceChildren(...entries.map(({ desktop, sha256, authority }) => {
962
982
  const [who, address] = desktop.includes('#') ? desktop.split('#', 2) : [desktop, ''];
963
- const title = address ? who === 'gateway' ? `The gateway ${(0, help_1.esc)(address)}` : who === desktops_1.DIRECT_KEY ? `${(0, help_1.esc)(address)} (direct)` : `${(0, help_1.esc)(address)} behind ${(0, help_1.esc)(who)}` : (0, help_1.esc)(who);
964
- // What else was decided about that server is said here, and forgotten with the certificate.
983
+ // A host's key as it reads; a machine reached through one is named as that host's ssh resolved it, which is
984
+ // its word: what could reorder or hide part of it shows as U+FFFD.
985
+ const host = (0, help_1.esc)((0, unshowable_1.showable)((0, desktops_1.shownKey)(who)));
986
+ const title = address ? who === 'gateway' ? `The gateway ${(0, help_1.esc)(address)}` : who === desktops_1.DIRECT_KEY ? `${(0, help_1.esc)(address)} (direct)` : `${(0, help_1.esc)(address)} behind ${host}` : host;
987
+ // How it came to be trusted, where that is known, and what else was decided about that server: forgotten with it.
988
+ const how = authority === true ? '<br>Valid for its name by a certificate authority' : authority === false ? '<br>Trusted by you' : '';
965
989
  const plain = this.desktop.signsInWithoutNla(desktop) ? '<br>Signs in without Network Level Authentication' : '';
966
- return this.row(title, `SHA-256 ${(0, help_1.esc)(sha256.slice(0, 23))}…${plain}`, [
990
+ return this.row(title, `SHA-256 ${(0, help_1.esc)(sha256.slice(0, 23))}…${how}${plain}`, [
967
991
  { label: 'Forget', run: () => this.desktop.forgetCertificate(desktop) },
968
992
  ]);
969
993
  }), this.empty(entries.length
970
- ? 'A forgotten certificate is remembered again, without asking, on the next connection.'
971
- : 'None yet. Windows and xrdp desktops\' certificates are remembered on first use and checked every time after; GNOME desktops only accept the one the plugin made.'));
994
+ ? 'A forgotten certificate is asked about again on the next connection, unless a certificate authority vouches for it; one behind an SSH host is remembered again without asking.'
995
+ : 'No saved certificates.'));
972
996
  }
973
997
  renderSessions() {
974
998
  const list = this.root.querySelector('[data-list=sessions]');
@@ -978,23 +1002,19 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
978
1002
  const sessions = this.desktop.sessionSummaries();
979
1003
  list.replaceChildren(...sessions.map(s => this.row((0, help_1.esc)(s.name), `${s.where ? `via ${(0, help_1.esc)(s.where)} · ` : ''}${(0, help_1.esc)(s.state)}`, [
980
1004
  { label: 'Copy log', run: () => this.copyLog(s.name, s.log) },
981
- ])), ...sessions.length ? [] : [this.empty('No desktop is open. Each open desktop\'s connection log can be copied from here, for a bug report.')]);
1005
+ ])), ...sessions.length ? [] : [this.empty('No desktops are open.')]);
982
1006
  }
983
1007
  copyLog(name, log) {
984
- const version = (() => {
985
- try {
986
- return require('../package.json').version;
987
- }
988
- catch {
989
- return '?';
990
- }
991
- })();
992
- this.platform.setClipboard({ text: [`tabby-rdp ${version} on ${process.platform}: ${name}`, ...log].join('\n') });
1008
+ const version = (0, updates_1.installedVersion)();
1009
+ this.platform.setClipboard({ text: [`tabby-rdp ${version} on ${process.platform}: ${name}`, ...this.compat.diagnosticLines(), ...log].join('\n') });
993
1010
  }
994
1011
  showTab(id) {
995
1012
  lastTab = TABS.some(t => t.id === id) ? id : TABS[0].id;
996
1013
  this.root.querySelectorAll('[data-tab]').forEach(el => el.classList.toggle('active', el.dataset.tab === lastTab));
997
1014
  this.root.querySelectorAll('[data-body]').forEach(el => { el.hidden = el.dataset.body !== lastTab; });
1015
+ if (lastTab === 'updates') {
1016
+ this.injector.get(updates_1.UpdateCheck).refresh();
1017
+ }
998
1018
  }
999
1019
  /** Shows the tab with a section (and opens a troubleshooting entry), with a short highlight. */
1000
1020
  reveal(topic, entry) {
@@ -1031,7 +1051,10 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
1031
1051
  return null;
1032
1052
  }
1033
1053
  }
1034
- /** "Remove…" on a remote desktop profile: asks, then deletes it as Tabby's Profiles page would (saved account and certificate go too). */
1054
+ /**
1055
+ * "Remove…" on a remote desktop profile: asks, then deletes it as Tabby's Profiles page would (a direct one's saved
1056
+ * passwords and certificate go too; see RDPProfilesService.deleteProfile).
1057
+ */
1035
1058
  async removeProfile(id) {
1036
1059
  const profiles = this.injector.get(tabby_core_1.ProfilesService);
1037
1060
  const profile = (await profiles.getProfiles()).find(p => p.id === id);
@@ -1041,7 +1064,10 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
1041
1064
  const { response } = await this.platform.showMessageBox({
1042
1065
  type: 'warning',
1043
1066
  message: `Remove the profile "${profile.name}"?`,
1044
- detail: 'Its saved account and remembered certificate are forgotten too.',
1067
+ // A saved account isn't the profile's: other desktops may sign in with it, and it stays.
1068
+ detail: profile.options?.via
1069
+ ? 'Its saved password and remembered certificate stay: they are those of the same desktop behind its SSH profile\'s host.'
1070
+ : 'Its saved password and remembered certificate are forgotten too.',
1045
1071
  buttons: ['Remove', 'Keep'],
1046
1072
  defaultId: 1,
1047
1073
  cancelId: 1,
@@ -1133,6 +1159,6 @@ exports.RemoteDesktopSettingsComponent = RemoteDesktopSettingsComponent = __deco
1133
1159
  tabby_core_1.PlatformService,
1134
1160
  core_1.Injector,
1135
1161
  core_1.NgZone,
1136
- tabby_core_1.HotkeysService])
1162
+ tabby_core_1.HotkeysService,
1163
+ hostCompat_1.HostCompatibility])
1137
1164
  ], RemoteDesktopSettingsComponent);
1138
- //# sourceMappingURL=settingsPage.js.map