From ecc9df766e18763975fffd734375e350521a2b5d Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Fri, 14 Aug 2026 03:56:16 -0700
Subject: [PATCH] fix(settings): stop truncating the SSH connection error to
one line
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The host key messages are written to be actionable — a mismatch ends in
"Run: ssh-keygen -R ", which is the remedy that also unblocks ssh and git.
The only place in the renderer that displays an SSH connection error clamped it
to a single line with `truncate` and carried no title attribute, so the remedy
was unreachable, not even on hover. The careful wording reached a CSS ellipsis.
Wraps instead, with [overflow-wrap:anywhere] because a long hostname offers no
break opportunity and would overflow the column on its own. The paragraph only
renders on failure, so the extra height costs nothing in the normal case.
CORRECTION to the four preceding commits: they each claimed 3 pre-existing
failures in ssh-connection-sftp-namespace.test.ts. That was my error — I had been
running `npx vitest` without `--config config/vitest.config.ts`, so the project's
setupFiles and execArgv were absent. Under the real config those 3 pass, and have
throughout. The whole SSH suite is green: 1,504 passed, 13 skipped.
Still failing on this branch and unrelated to it (different subsystems, no file
overlap): 5 in terminal-snapshot-osc8-roundtrip and 2 in browser-cookie-import.
---
.../settings/SshTargetCard.test.tsx | 84 +++++++++++++++++++
.../src/components/settings/SshTargetCard.tsx | 4 +-
2 files changed, 87 insertions(+), 1 deletion(-)
create mode 100644 src/renderer/src/components/settings/SshTargetCard.test.tsx
diff --git a/src/renderer/src/components/settings/SshTargetCard.test.tsx b/src/renderer/src/components/settings/SshTargetCard.test.tsx
new file mode 100644
index 00000000000..6bcdfd45a1e
--- /dev/null
+++ b/src/renderer/src/components/settings/SshTargetCard.test.tsx
@@ -0,0 +1,84 @@
+// @vitest-environment happy-dom
+
+import { act } from 'react'
+import { createRoot } from 'react-dom/client'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { SshTargetCard } from './SshTargetCard'
+import { TooltipProvider } from '../ui/tooltip'
+import type { SshConnectionState, SshTarget } from '../../../../shared/ssh-types'
+
+afterEach(() => {
+ document.body.innerHTML = ''
+})
+
+const target: SshTarget = {
+ id: 'target-1',
+ label: 'build-01',
+ host: 'build-01.internal',
+ port: 22,
+ username: 'deploy'
+}
+
+/** The real thing a host key mismatch produces: the remedy is the last clause. */
+const HOST_KEY_ERROR =
+ 'Host key verification failed for build-01.internal. The key does not match the entry in your known_hosts file. ssh and git will refuse this host too. Run: ssh-keygen -R build-01.internal'
+
+async function renderCard(state: SshConnectionState | undefined): Promise {
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+ await act(async () => {
+ createRoot(container).render(
+
+
+
+ )
+ })
+ return container
+}
+
+const errorState = (error: string): SshConnectionState => ({
+ targetId: 'target-1',
+ status: 'error',
+ error,
+ reconnectAttempt: 0,
+ supportsFolderDownload: false
+})
+
+describe('the connection error on an SSH target card', () => {
+ it('shows the whole message, remedy included', async () => {
+ const container = await renderCard(errorState(HOST_KEY_ERROR))
+
+ expect(container.textContent).toContain('ssh-keygen -R build-01.internal')
+ })
+
+ // This is the regression that made the careful wording pointless: `truncate` clamps to one line
+ // with an ellipsis and there is no title attribute, so the remedy was unreachable even on hover.
+ it('does not clamp it to a single line', async () => {
+ const container = await renderCard(errorState(HOST_KEY_ERROR))
+ const paragraph = [...container.querySelectorAll('p')].find((node) =>
+ node.textContent?.includes('ssh-keygen')
+ )
+
+ expect(paragraph).toBeDefined()
+ expect(paragraph?.className).not.toContain('truncate')
+ // A long hostname has no break opportunity, so wrapping alone would still overflow the column.
+ expect(paragraph?.className).toContain('[overflow-wrap:anywhere]')
+ })
+
+ it('renders nothing when there is no error', async () => {
+ const container = await renderCard(undefined)
+
+ expect(container.textContent).not.toContain('Host key verification failed')
+ })
+})
diff --git a/src/renderer/src/components/settings/SshTargetCard.tsx b/src/renderer/src/components/settings/SshTargetCard.tsx
index 6a99f0fe648..bf9043b68ec 100644
--- a/src/renderer/src/components/settings/SshTargetCard.tsx
+++ b/src/renderer/src/components/settings/SshTargetCard.tsx
@@ -301,8 +301,10 @@ export function SshTargetCard({
{target.identityFile ? ` \u2022 ${target.identityFile}` : ''}
{` \u2022 ${terminalPersistence}`}
+ {/* Why not truncate: host key failures put the remedy (`ssh-keygen -R `) at the end,
+ and a one-line clamp with no tooltip made it unreachable even on hover. */}
{state?.error ? (
- {state.error}
+ {state.error}
) : null}