List ~/.ssh/config hosts in the panel source dropdown

Read-only group below saved connections; hosts resolve HostName/User/
Port/IdentityFile (Include, wildcards, %-tokens) and connect on click.
New ssh-agent auth kind covers hosts without a usable identity file;
ProxyJump/ProxyCommand hosts are shown disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 14:10:52 +02:00
co-authored by Claude Fable 5
parent fdf65766b5
commit f4819a58fe
11 changed files with 240 additions and 18 deletions
@@ -28,7 +28,8 @@ function toDraft(c: SavedConnection): Draft {
host: c.host,
port: c.port,
username: c.username,
authKind: c.auth.kind,
// agent auth only exists on ~/.ssh/config hosts, which never reach this form
authKind: c.auth.kind === 'password' ? 'password' : 'key',
password: c.auth.kind === 'password' ? c.auth.password : '',
privateKeyPath: c.auth.kind === 'key' ? c.auth.privateKeyPath : '',
passphrase: c.auth.kind === 'key' ? (c.auth.passphrase ?? '') : '',
+13 -2
View File
@@ -1,5 +1,5 @@
import { Injectable, computed, inject, signal } from '@angular/core';
import type { ConnFolder, SavedConnection } from '@shared/types';
import type { ConnFolder, SavedConnection, SshConfigHost } from '@shared/types';
import { RpcService } from './rpc.service';
import { ToastService } from './toast.service';
@@ -10,6 +10,8 @@ export class ConnectionsService {
readonly folders = signal<ConnFolder[]>([]);
readonly connections = signal<SavedConnection[]>([]);
/** Read-only hosts from ~/.ssh/config; empty when the file is absent or unreadable. */
readonly sshHosts = signal<SshConfigHost[]>([]);
/** Root-level connections plus one group per folder (folders always shown). */
readonly grouped = computed(() => {
@@ -24,7 +26,16 @@ export class ConnectionsService {
});
constructor() {
if (this.rpc.available) void this.reload();
if (this.rpc.available) {
void this.reload();
void this.reloadSshHosts();
}
}
/** Silent on failure — a broken ssh config just means an empty group. */
async reloadSshHosts(): Promise<void> {
const res = await this.rpc.request.getSshConfigHosts({});
this.sshHosts.set(res.ok ? res.hosts : []);
}
async reload(): Promise<void> {
+19 -1
View File
@@ -1,7 +1,7 @@
<!-- session strip -->
<div class="relative flex items-center gap-1.5 border-b border-border px-2 py-1">
<button
(click)="menuOpen.set(!menuOpen())"
(click)="toggleMenu()"
class="flex min-w-0 items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium hover:bg-accent"
title="Choose what this panel shows"
>
@@ -128,6 +128,24 @@
}
}
@if (connections.sshHosts().length) {
<div class="my-1 border-t border-border"></div>
<div class="px-3 pt-1.5 pb-0.5 text-[10px] font-medium tracking-wide text-muted-foreground uppercase">
~/.ssh/config
</div>
@for (host of connections.sshHosts(); track host.name) {
<button
(click)="pickSshHost(host)"
[disabled]="host.unsupported"
[title]="sshHostTitle(host)"
class="flex w-full items-center gap-2 py-1.5 pr-3 pl-5 text-left text-xs hover:bg-accent disabled:opacity-60 disabled:hover:bg-transparent"
>
<ng-icon name="mynaServers" size="0.9rem" class="shrink-0 text-muted-foreground" />
<span class="flex-1 truncate">{{ host.name }}</span>
</button>
}
}
<div class="my-1 border-t border-border"></div>
<button
(click)="openManager()"
+30 -1
View File
@@ -9,7 +9,7 @@ import {
viewChild,
} from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import type { FileEntry, SavedConnection } from '@shared/types';
import type { FileEntry, SavedConnection, SshConfigHost } from '@shared/types';
import { ConnectionsService } from '../connections.service';
import { DialogsService } from '../dialogs.service';
import { FileActionsService } from '../file-actions.service';
@@ -84,6 +84,35 @@ export class FilePanel {
void this.panels.connectPanel(this.side(), connection, connection.name);
}
toggleMenu(): void {
this.menuOpen.set(!this.menuOpen());
// re-read ~/.ssh/config on each open so external edits show up
if (this.menuOpen()) void this.connections.reloadSshHosts();
}
pickSshHost(host: SshConfigHost): void {
this.menuOpen.set(false);
this.activate();
void this.panels.connectPanel(
this.side(),
{
host: host.hostname,
port: host.port,
username: host.username,
auth: host.identityFile
? { kind: 'key', privateKeyPath: host.identityFile }
: { kind: 'agent' },
initialPath: null,
},
host.name,
);
}
sshHostTitle(host: SshConfigHost): string {
if (host.unsupported === 'proxy') return 'Uses ProxyJump/ProxyCommand — not supported';
return `${host.username}@${host.hostname}:${host.port}`;
}
openManager(): void {
this.menuOpen.set(false);
this.activate();