From feba745ea75458d6a34c23b7dd4d5e3f1df15fcc Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:11:28 +0800 Subject: [PATCH 1/7] feat(gui): show the installed version in Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings had no version anywhere, but a bug report for the desktop app has to quote one, and the only way to get it was to leave the app and run `omny --version` in the terminal app. An About section at the foot of Settings now names the installed build. The value comes from Tauri's own `getVersion`, which reads the CARGO_PKG_VERSION tauri-codegen bakes into the context — the same number `omny --version` prints, since both crates take the workspace version. `core:default` already carries `core:app:allow-version`, so no capability changes. The read is wrapped in a dynamic import like the theme store's native calls, so Vitest and `vite preview` (no Tauri runtime) still load. It is kept out of the update-config load: a version that will not read leaves the row saying so rather than taking the update preferences down with it. Closes #125. --- CHANGELOG.md | 7 ++++ crates/omnyssh-gui/ui/e2e/settings.spec.ts | 37 +++++++++++++++++-- .../ui/src/lib/ipc/appInfo.test.ts | 24 ++++++++++++ crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts | 12 ++++++ .../ui/src/lib/screens/Settings.svelte | 30 +++++++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts create mode 100644 crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ba920f..0766fa31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ Versions follow [Semantic Versioning](https://semver.org/). --- +## Unreleased + +### Features +- **The desktop app says which version it is.** Settings had no version anywhere, and a bug report for it has to quote one: the only way to get it was to leave the app and run `omny --version` in the terminal app. An About section in Settings now names the installed build, taken from the same version the terminal app prints, so the two cannot disagree. The number is shown whether or not an update is pending, since it is what a report needs before deciding to install one. + +--- + ## 1.1.4 — 2026-09-28 ### Bug Fixes diff --git a/crates/omnyssh-gui/ui/e2e/settings.spec.ts b/crates/omnyssh-gui/ui/e2e/settings.spec.ts index 80da25ab..26feb998 100644 --- a/crates/omnyssh-gui/ui/e2e/settings.spec.ts +++ b/crates/omnyssh-gui/ui/e2e/settings.spec.ts @@ -4,7 +4,8 @@ import { expect, test, type Page } from '@playwright/test'; // absent, so we stub `__TAURI_INTERNALS__` at the boundary (§6.4). The stub backs the // update config in memory and returns an update from `check_update`; `update-available` // is fired after `reload_hosts` (which the layout calls once its listeners are attached), -// mirroring the startup check. +// mirroring the startup check. It also answers `plugin:app|version`, the call behind the +// About row (issue #125), which Tauri's own `getVersion` makes off-runtime. const HOSTS = [ { name: 'web-1', hostname: 'web-1.example.com', user: 'deploy', port: 22, tags: [], source: 'manual', hasKey: true, localForwards: [], tunnelAutostart: false, forwardAgent: false } ]; @@ -16,15 +17,19 @@ const UPDATE = { canSelfUpdate: true }; +const INSTALLED = '1.1.4'; + async function boot( page: Page, opts: { fireUpdateOnBoot: boolean; traySupport?: { available: boolean; minimize: boolean }; + /** Make the version unreadable, as off-runtime the About row must still render. */ + versionFails?: boolean; } ): Promise { await page.addInitScript( - ({ hosts, update, fireUpdateOnBoot, traySupport }) => { + ({ hosts, update, installed, fireUpdateOnBoot, traySupport, versionFails }) => { let cbid = 0; const listeners: Record = {}; const state = { @@ -61,6 +66,10 @@ async function boot( return Promise.resolve(null); case 'check_update': return Promise.resolve({ ...update }); + case 'plugin:app|version': + return versionFails + ? Promise.reject('no Tauri runtime') + : Promise.resolve(installed); case 'set_tray_behavior': ((win.__tray ??= []) as unknown[]).push({ ...args }); return Promise.resolve({ ...traySupport }); @@ -83,8 +92,10 @@ async function boot( { hosts: HOSTS, update: UPDATE, + installed: INSTALLED, fireUpdateOnBoot: opts.fireUpdateOnBoot, - traySupport: opts.traySupport ?? { available: true, minimize: true } + traySupport: opts.traySupport ?? { available: true, minimize: true }, + versionFails: opts.versionFails ?? false } ); await page.goto('/'); @@ -121,6 +132,26 @@ test('the footer gear opens Settings; theme, interval, and update prefs work', a await expect(page.getByText('Update available — v2.0.0')).toBeVisible(); }); +test('the About row names the installed build, and says so when it cannot be read', async ({ + page +}) => { + // The version a bug report has to quote (issue #125). Distinct from the "Version 2.0.0 + // is available." line above, which is the newest release rather than this build. + await boot(page, { fireUpdateOnBoot: false }); + await page.getByRole('button', { name: 'Settings' }).click(); + await expect(page.getByRole('heading', { name: 'About' })).toBeVisible(); + await expect(page.getByText(`Version ${INSTALLED}`, { exact: true })).toBeVisible(); + + // A version that will not read must not take the rest of Settings with it: the row + // says so and the update preferences still load. + await boot(page, { fireUpdateOnBoot: false, versionFails: true }); + await page.getByRole('button', { name: 'Settings' }).click(); + await expect(page.getByText('Version unavailable')).toBeVisible(); + await expect( + page.getByRole('switch', { name: 'Check for updates on startup' }) + ).toHaveAttribute('aria-checked', 'true'); +}); + test('startup update-available raises the banner; dismiss hides it', async ({ page }) => { await boot(page, { fireUpdateOnBoot: true }); diff --git a/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts new file mode 100644 index 00000000..daec4c71 --- /dev/null +++ b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it, vi } from 'vitest'; + +// The About row (issue #125) is the one place the running build is named, and a bug +// report has to quote it. The value is Tauri's own `getVersion` — the crate's +// `CARGO_PKG_VERSION`, which is the same number `omny --version` prints — so nothing +// on the frontend can drift from the binary. The mock stands in for the native call. +const getVersion = vi.fn(async () => '1.1.4'); +vi.mock('@tauri-apps/api/app', () => ({ getVersion: () => getVersion() })); + +describe('installedVersion', () => { + it('reports the version the native call returns', async () => { + const { installedVersion } = await import('./appInfo'); + expect(await installedVersion()).toBe('1.1.4'); + }); + + // The import is dynamic, so a failed call surfaces as a rejection the Settings screen + // catches — it must not throw at module load, where there is no Tauri runtime at all + // (Vitest, `vite preview`) and nothing could catch it. + it('propagates a failed call instead of swallowing it', async () => { + getVersion.mockRejectedValueOnce('no Tauri runtime'); + const { installedVersion } = await import('./appInfo'); + await expect(installedVersion()).rejects.toThrow(); + }); +}); diff --git a/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts new file mode 100644 index 00000000..5bd7306f --- /dev/null +++ b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts @@ -0,0 +1,12 @@ +// The running build's version, from Tauri's own `getVersion` (the `CARGO_PKG_VERSION` +// tauri-codegen bakes into the context — the same 1.1.4 the terminal app's +// `omny --version` prints, since both crates take the workspace version). +// +// `core:default` already carries `core:app:allow-version`, so this needs no new +// capability. Dynamically imported so Vitest / `vite preview` (no Tauri runtime) +// don't fail at load, matching the theme store's native-call pattern (tech-gui.md +// §5.1). Rejects off-runtime; the caller decides what an unreadable version means. +export async function installedVersion(): Promise { + const { getVersion } = await import('@tauri-apps/api/app'); + return getVersion(); +} diff --git a/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte b/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte index bc2e198a..b8edfb2f 100644 --- a/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte +++ b/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte @@ -3,6 +3,8 @@ // (§5.1), the metric auto-refresh interval and the tray (tauri-plugin-store UI prefs), // and the update preferences (`check_on_startup`) + a manual update check. Update prefs // persist to the shared config via `save_update_config`; the rest are frontend prefs. + // The installed version rides along with the update preferences (issue 125): it is the + // one number a bug report has to quote, and the only place it is ever shown. import { onMount } from 'svelte'; import type { UpdateConfigDto } from '$lib/bindings'; import { Surface, Icon } from '$lib/theme'; @@ -14,11 +16,13 @@ import { offerUpdate } from '$lib/stores/update'; import { lastError } from '$lib/stores/notifications'; import { checkUpdate, loadUpdateConfig, saveUpdateConfig } from '$lib/ipc/commands'; + import { installedVersion } from '$lib/ipc/appInfo'; const message = (e: unknown): string => (e instanceof Error ? e.message : String(e)); const formatInterval = (secs: number): string => (secs < 60 ? `${secs}s` : `${secs / 60}m`); let updateConfig = $state(null); + let version = $state(null); type CheckState = | { kind: 'idle' } | { kind: 'checking' } @@ -28,6 +32,15 @@ let check = $state({ kind: 'idle' }); onMount(async () => { + // Read apart from the update config, so a version that will not read leaves the rest + // of the screen working: the About row says "unavailable" and the update preferences + // still load. Nothing is raised — the app is running, so it has a version; this is + // only the case where the call itself is refused. + try { + version = await installedVersion(); + } catch { + version = null; + } try { updateConfig = await loadUpdateConfig(); } catch (e) { @@ -250,6 +263,23 @@ + + + +

About

+
+
+

OmnySSH Desktop

+

Free and open source, with no paid tier.

+
+

+ {version ? `Version ${version}` : 'Version unavailable'} +

+
+
From 4a54242e6bad7b3c1f3ecb8c6c249f6251bd980c Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Thu, 1 Oct 2026 20:50:22 +0400 Subject: [PATCH 2/7] fix(gui): pare About down to a copyable version row --- crates/omnyssh-gui/ui/e2e/settings.spec.ts | 28 +++++++------- .../ui/src/lib/ipc/appInfo.test.ts | 24 ------------ crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts | 16 ++------ .../ui/src/lib/screens/Settings.svelte | 37 ++++++------------- 4 files changed, 29 insertions(+), 76 deletions(-) delete mode 100644 crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts diff --git a/crates/omnyssh-gui/ui/e2e/settings.spec.ts b/crates/omnyssh-gui/ui/e2e/settings.spec.ts index 26feb998..2da860ca 100644 --- a/crates/omnyssh-gui/ui/e2e/settings.spec.ts +++ b/crates/omnyssh-gui/ui/e2e/settings.spec.ts @@ -4,8 +4,7 @@ import { expect, test, type Page } from '@playwright/test'; // absent, so we stub `__TAURI_INTERNALS__` at the boundary (§6.4). The stub backs the // update config in memory and returns an update from `check_update`; `update-available` // is fired after `reload_hosts` (which the layout calls once its listeners are attached), -// mirroring the startup check. It also answers `plugin:app|version`, the call behind the -// About row (issue #125), which Tauri's own `getVersion` makes off-runtime. +// mirroring the startup check. `plugin:app|version` is Tauri's `getVersion`. const HOSTS = [ { name: 'web-1', hostname: 'web-1.example.com', user: 'deploy', port: 22, tags: [], source: 'manual', hasKey: true, localForwards: [], tunnelAutostart: false, forwardAgent: false } ]; @@ -17,14 +16,13 @@ const UPDATE = { canSelfUpdate: true }; -const INSTALLED = '1.1.4'; +const INSTALLED = '1.0.0'; async function boot( page: Page, opts: { fireUpdateOnBoot: boolean; traySupport?: { available: boolean; minimize: boolean }; - /** Make the version unreadable, as off-runtime the About row must still render. */ versionFails?: boolean; } ): Promise { @@ -132,24 +130,26 @@ test('the footer gear opens Settings; theme, interval, and update prefs work', a await expect(page.getByText('Update available — v2.0.0')).toBeVisible(); }); -test('the About row names the installed build, and says so when it cannot be read', async ({ - page -}) => { - // The version a bug report has to quote (issue #125). Distinct from the "Version 2.0.0 - // is available." line above, which is the newest release rather than this build. +test('About shows the installed version, selectable for a bug report', async ({ page }) => { await boot(page, { fireUpdateOnBoot: false }); await page.getByRole('button', { name: 'Settings' }).click(); - await expect(page.getByRole('heading', { name: 'About' })).toBeVisible(); - await expect(page.getByText(`Version ${INSTALLED}`, { exact: true })).toBeVisible(); - // A version that will not read must not take the rest of Settings with it: the row - // says so and the update preferences still load. + const about = page.getByRole('heading', { name: 'About' }).locator('..'); + await expect(about.getByText('Version', { exact: true })).toBeVisible(); + const version = about.getByText(INSTALLED, { exact: true }); + await expect(version).toBeVisible(); + await expect(version).toHaveCSS('user-select', 'text'); +}); + +test('a failed version read leaves no row, and the update prefs still load', async ({ page }) => { await boot(page, { fireUpdateOnBoot: false, versionFails: true }); await page.getByRole('button', { name: 'Settings' }).click(); - await expect(page.getByText('Version unavailable')).toBeVisible(); + await expect( page.getByRole('switch', { name: 'Check for updates on startup' }) ).toHaveAttribute('aria-checked', 'true'); + await expect(page.getByRole('heading', { name: 'About' })).toHaveCount(0); + await expect(page.getByText('Version', { exact: true })).toHaveCount(0); }); test('startup update-available raises the banner; dismiss hides it', async ({ page }) => { diff --git a/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts deleted file mode 100644 index daec4c71..00000000 --- a/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -// The About row (issue #125) is the one place the running build is named, and a bug -// report has to quote it. The value is Tauri's own `getVersion` — the crate's -// `CARGO_PKG_VERSION`, which is the same number `omny --version` prints — so nothing -// on the frontend can drift from the binary. The mock stands in for the native call. -const getVersion = vi.fn(async () => '1.1.4'); -vi.mock('@tauri-apps/api/app', () => ({ getVersion: () => getVersion() })); - -describe('installedVersion', () => { - it('reports the version the native call returns', async () => { - const { installedVersion } = await import('./appInfo'); - expect(await installedVersion()).toBe('1.1.4'); - }); - - // The import is dynamic, so a failed call surfaces as a rejection the Settings screen - // catches — it must not throw at module load, where there is no Tauri runtime at all - // (Vitest, `vite preview`) and nothing could catch it. - it('propagates a failed call instead of swallowing it', async () => { - getVersion.mockRejectedValueOnce('no Tauri runtime'); - const { installedVersion } = await import('./appInfo'); - await expect(installedVersion()).rejects.toThrow(); - }); -}); diff --git a/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts index 5bd7306f..c460ad9f 100644 --- a/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts +++ b/crates/omnyssh-gui/ui/src/lib/ipc/appInfo.ts @@ -1,12 +1,4 @@ -// The running build's version, from Tauri's own `getVersion` (the `CARGO_PKG_VERSION` -// tauri-codegen bakes into the context — the same 1.1.4 the terminal app's -// `omny --version` prints, since both crates take the workspace version). -// -// `core:default` already carries `core:app:allow-version`, so this needs no new -// capability. Dynamically imported so Vitest / `vite preview` (no Tauri runtime) -// don't fail at load, matching the theme store's native-call pattern (tech-gui.md -// §5.1). Rejects off-runtime; the caller decides what an unreadable version means. -export async function installedVersion(): Promise { - const { getVersion } = await import('@tauri-apps/api/app'); - return getVersion(); -} +import { getVersion } from '@tauri-apps/api/app'; + +// The version baked into the binary: the workspace one, as `omny --version` prints it. +export const installedVersion = (): Promise => getVersion(); diff --git a/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte b/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte index b8edfb2f..17582b37 100644 --- a/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte +++ b/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte @@ -3,8 +3,6 @@ // (§5.1), the metric auto-refresh interval and the tray (tauri-plugin-store UI prefs), // and the update preferences (`check_on_startup`) + a manual update check. Update prefs // persist to the shared config via `save_update_config`; the rest are frontend prefs. - // The installed version rides along with the update preferences (issue 125): it is the - // one number a bug report has to quote, and the only place it is ever shown. import { onMount } from 'svelte'; import type { UpdateConfigDto } from '$lib/bindings'; import { Surface, Icon } from '$lib/theme'; @@ -32,15 +30,8 @@ let check = $state({ kind: 'idle' }); onMount(async () => { - // Read apart from the update config, so a version that will not read leaves the rest - // of the screen working: the About row says "unavailable" and the update preferences - // still load. Nothing is raised — the app is running, so it has a version; this is - // only the case where the call itself is refused. - try { - version = await installedVersion(); - } catch { - version = null; - } + // Not awaited, so the update prefs never wait on it; a failed read leaves the row out. + installedVersion().then((v) => (version = v), () => {}); try { updateConfig = await loadUpdateConfig(); } catch (e) { @@ -264,22 +255,16 @@ - - -

About

-
-
-

OmnySSH Desktop

-

Free and open source, with no paid tier.

+ + {#if version} + +

About

+
+

Version

+

{version}

-

- {version ? `Version ${version}` : 'Version unavailable'} -

-
- + + {/if}
From 82100e28ba34c2975e623ae86a675a067a7dcd61 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Thu, 1 Oct 2026 20:50:22 +0400 Subject: [PATCH 3/7] fix(gui): keep the bottom padding in Settings --- crates/omnyssh-gui/ui/e2e/settings.spec.ts | 19 +++++++++++++++++++ .../ui/src/lib/screens/Settings.svelte | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/omnyssh-gui/ui/e2e/settings.spec.ts b/crates/omnyssh-gui/ui/e2e/settings.spec.ts index 2da860ca..5c038bf7 100644 --- a/crates/omnyssh-gui/ui/e2e/settings.spec.ts +++ b/crates/omnyssh-gui/ui/e2e/settings.spec.ts @@ -152,6 +152,25 @@ test('a failed version read leaves no row, and the update prefs still load', asy await expect(page.getByText('Version', { exact: true })).toHaveCount(0); }); +test('scrolled to the end, Settings keeps its bottom padding', async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 480 }); + await boot(page, { fireUpdateOnBoot: false }); + await page.getByRole('button', { name: 'Settings' }).click(); + + const last = page.getByRole('heading', { name: 'About' }).locator('..'); + await expect(last).toBeVisible(); + const { overflows, gap } = await last.evaluate((card) => { + const scroller = card.closest('.overflow-auto') as HTMLElement; + scroller.scrollTop = scroller.scrollHeight; + return { + overflows: scroller.scrollHeight > scroller.clientHeight, + gap: scroller.getBoundingClientRect().bottom - card.getBoundingClientRect().bottom + }; + }); + expect(overflows).toBe(true); + expect(gap).toBeGreaterThanOrEqual(24); +}); + test('startup update-available raises the banner; dismiss hides it', async ({ page }) => { await boot(page, { fireUpdateOnBoot: true }); diff --git a/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte b/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte index 17582b37..b7cb61fd 100644 --- a/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte +++ b/crates/omnyssh-gui/ui/src/lib/screens/Settings.svelte @@ -80,7 +80,7 @@ active ? 'bg-accent text-accent-fg' : 'text-muted hover:bg-surface-inset hover:text-fg'; -
+

Settings

From d8f1c91a861fca604e01e2d0252b2f859b684878 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Thu, 1 Oct 2026 20:50:30 +0400 Subject: [PATCH 4/7] docs(changelog): trim the Settings version entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58558c7c..d2896e11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Versions follow [Semantic Versioning](https://semver.org/). ## Unreleased ### Features -- **The desktop app says which version it is.** Settings had no version anywhere, and a bug report for it has to quote one: the only way to get it was to leave the app and run `omny --version` in the terminal app. An About section in Settings now names the installed build, taken from the same version the terminal app prints, so the two cannot disagree. The number is shown whether or not an update is pending, since it is what a report needs before deciding to install one. +- **Settings shows the installed version.** On Windows and Linux the desktop app showed its version nowhere. Settings now ends with it, and it can be copied into a bug report. ### Bug Fixes - **Security: a server that offers a host key of another type than the one saved for it is refused.** OmnySSH compared the offered key only with saved keys of the same type. A host saved with its Ed25519 key that suddenly offered only an RSA or ECDSA key was therefore taken for a new host: the key was saved and the login went ahead, password included, and the dashboard did this in the background without anyone connecting. Someone intercepting the connection could get past the saved key this way. Like `ssh`, OmnySSH now refuses any key that matches none of those saved for the host, whatever its type, and says which type is saved. A server that really dropped the type of key it was saved with needs its old key removed once, as `ssh` also asks. From 09e7d4e2d589b672a5b765bd376184a10de2344d Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Thu, 1 Oct 2026 20:50:30 +0400 Subject: [PATCH 5/7] docs: point bug reports at the version in Settings --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f86dfda4..fbd7a1a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -258,7 +258,7 @@ chore: bump ratatui to 0.29 Please open a GitHub Issue with: -- OmnySSH version (`omny --version`) +- OmnySSH version (`omny --version`, or Settings in the desktop app) - OS and terminal emulator - Steps to reproduce - Expected behaviour vs. actual behaviour From 8a45204dbdcee82f7601a5cf6cd80ef10aa4da34 Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Thu, 1 Oct 2026 21:01:34 +0400 Subject: [PATCH 6/7] test(gui): pin that a hung version read never holds up Settings --- crates/omnyssh-gui/ui/e2e/settings.spec.ts | 34 ++++++++++++---------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/omnyssh-gui/ui/e2e/settings.spec.ts b/crates/omnyssh-gui/ui/e2e/settings.spec.ts index 5c038bf7..25697a06 100644 --- a/crates/omnyssh-gui/ui/e2e/settings.spec.ts +++ b/crates/omnyssh-gui/ui/e2e/settings.spec.ts @@ -23,11 +23,11 @@ async function boot( opts: { fireUpdateOnBoot: boolean; traySupport?: { available: boolean; minimize: boolean }; - versionFails?: boolean; + version?: 'fails' | 'hangs'; } ): Promise { await page.addInitScript( - ({ hosts, update, installed, fireUpdateOnBoot, traySupport, versionFails }) => { + ({ hosts, update, installed, fireUpdateOnBoot, traySupport, version }) => { let cbid = 0; const listeners: Record = {}; const state = { @@ -65,9 +65,9 @@ async function boot( case 'check_update': return Promise.resolve({ ...update }); case 'plugin:app|version': - return versionFails - ? Promise.reject('no Tauri runtime') - : Promise.resolve(installed); + if (version === 'fails') return Promise.reject('no Tauri runtime'); + if (version === 'hangs') return new Promise(() => {}); + return Promise.resolve(installed); case 'set_tray_behavior': ((win.__tray ??= []) as unknown[]).push({ ...args }); return Promise.resolve({ ...traySupport }); @@ -93,7 +93,7 @@ async function boot( installed: INSTALLED, fireUpdateOnBoot: opts.fireUpdateOnBoot, traySupport: opts.traySupport ?? { available: true, minimize: true }, - versionFails: opts.versionFails ?? false + version: opts.version } ); await page.goto('/'); @@ -141,16 +141,20 @@ test('About shows the installed version, selectable for a bug report', async ({ await expect(version).toHaveCSS('user-select', 'text'); }); -test('a failed version read leaves no row, and the update prefs still load', async ({ page }) => { - await boot(page, { fireUpdateOnBoot: false, versionFails: true }); - await page.getByRole('button', { name: 'Settings' }).click(); +for (const version of ['fails', 'hangs'] as const) { + test(`a version read that ${version} leaves no row, and the update prefs still load`, async ({ + page + }) => { + await boot(page, { fireUpdateOnBoot: false, version }); + await page.getByRole('button', { name: 'Settings' }).click(); - await expect( - page.getByRole('switch', { name: 'Check for updates on startup' }) - ).toHaveAttribute('aria-checked', 'true'); - await expect(page.getByRole('heading', { name: 'About' })).toHaveCount(0); - await expect(page.getByText('Version', { exact: true })).toHaveCount(0); -}); + await expect( + page.getByRole('switch', { name: 'Check for updates on startup' }) + ).toHaveAttribute('aria-checked', 'true'); + await expect(page.getByRole('heading', { name: 'About' })).toHaveCount(0); + await expect(page.getByText('Version', { exact: true })).toHaveCount(0); + }); +} test('scrolled to the end, Settings keeps its bottom padding', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 480 }); From c315cefcdb176e15f64ccd9eebc2c25b029681db Mon Sep 17 00:00:00 2001 From: Tim Hartmann Date: Thu, 1 Oct 2026 21:01:34 +0400 Subject: [PATCH 7/7] docs(changelog): note the Settings bottom margin fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2896e11..dd9f95f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Versions follow [Semantic Versioning](https://semver.org/). - **A server with no algorithm in common says which kind is missing and what it offers.** The error read "No common algorithm", and a key exchange mismatch even "Unknown algorithm", without saying whether the key exchange, host key, cipher or MAC was the problem, so there was nothing to go on. It now reads, for example, "no common cipher; the server offers aes128-cbc". Servers that offer only methods OmnySSH leaves out on purpose, such as SHA-1 Diffie-Hellman or CBC ciphers, still do not connect. - **macOS: a host on your local network that fails with "No route to host" says macOS may be blocking it.** macOS keeps an app away from devices on the local network until you allow it, and a blocked connection fails at once with "No route to host (os error 65)", while the router usually stays reachable because it is also your DNS server, so it looked like a fault on that one machine. The error now adds that macOS may be blocking the app and where to allow it: System Settings, Privacy & Security, Local Network, then reopen the app. The desktop app also now carries the explanation macOS shows when it asks for that permission. The terminal app needs the permission for the terminal it runs in; Apple's Terminal has it already. - **A terminal the server closes right after login stays open and shows why.** A Synology NAS gives a shell only to administrators: any other account logs in, gets DSM's "Permission denied, please try again." and the session ends. `ssh` shows that message, but OmnySSH closed the tab the moment the session ended, so the terminal seemed to vanish right after the password while file sessions to the same NAS worked. In both apps, a terminal tab that showed anything now stays open when the remote side ends its session, including after `exit` or a dropped connection, with "[Connection closed. Press Enter to close this tab.]" under the last output. Enter or Esc closes it, and it takes no other input. A terminal that fails before showing anything still closes, with the reason in the status bar. The desktop app could also drop the last lines a session printed just before it ended; they now always arrive. +- **Settings keeps its bottom margin when scrolled to the end.** The last section sat flush against the bottom of the window. ---