refactor(ssh): rebuild Settings → SSH as a host library

The section was a field editor wearing a list's clothes: the widest
column on the page said "Select a profile to edit" and nothing else, the
global security toggles hung underneath whichever host happened to be
open, and port forwards had to be typed as `L bind:port target:port` into
a text area that taught the syntax nowhere and silently dropped any line
it couldn't parse.

The master column now leads with a title and a filter — past a dozen
hosts, finding one is the job — with Add and the `~/.ssh/config` import
demoted to icon affordances beside it. Hosts bucket by group into
collapsible sections; a collapsed header keeps showing how many of its
hosts are connected, and collapsing the group holding the selection hands
the detail pane back to Defaults rather than stranding a selected row
nobody can see. A live query force-expands every group.

`Defaults` is now a pinned row with its own page, so "every host starts
from these and can override one" is something the list's shape says. Each
per-host override names the value it currently inherits instead of
claiming a mechanism exists.

A selected host leads with its own name, address, jump chain and live
status, then Save (disabled with nothing to write) and Connect. Save
leaves the form open — with the list permanently beside it, closing back
to an empty pane read as the selection being thrown away.

Port forwards are one row of inputs per rule, and a rule that can't
connect is flagged in place rather than dropped on save.

With nothing selected the pane offers quick connect and, when
`~/.ssh/config` holds aliases tty7 hasn't linked, an offer to link them.
This commit is contained in:
l0ng-ai
2026-07-28 18:38:23 +08:00
parent c469e10312
commit f0cfb47b06
2 changed files with 1172 additions and 315 deletions
+39
View File
@@ -4339,6 +4339,29 @@ impl Tty7App {
}),
);
// Live filter for the SSH section's host list; each keystroke re-renders
// the master column so the list narrows as you type.
let ssh_filter = cx.new(|cx| InputState::new(window, cx).placeholder("Filter hosts…"));
subs.push(
cx.subscribe_in(&ssh_filter, window, |_this, _i, ev, _w, cx| {
if matches!(ev, InputEvent::Change) {
cx.notify();
}
}),
);
// The SSH empty state's quick-connect box. Its Connect button enables only
// on a parsable target, so each keystroke re-renders the pane.
let ssh_quick_connect =
cx.new(|cx| InputState::new(window, cx).placeholder("user@host or user@host:port"));
subs.push(
cx.subscribe_in(&ssh_quick_connect, window, |_this, _i, ev, _w, cx| {
if matches!(ev, InputEvent::Change) {
cx.notify();
}
}),
);
self.settings = Some(SettingsState {
focus_handle: focus_handle.clone(),
section: SettingsSection::Appearance,
@@ -4360,6 +4383,9 @@ impl Tty7App {
rebinding_note: None,
ssh_form: None,
ssh_detail: crate::ui::settings::SshDetail::None,
ssh_filter,
ssh_collapsed_groups: std::collections::HashSet::new(),
ssh_quick_connect,
agent_hooks_host: crate::ui::host_ops::HostId::LOCAL,
agent_hooks_states: crate::ui::settings::AgentHooksView::Loading,
agent_hooks_seq: 0,
@@ -7291,6 +7317,19 @@ mod keybinding_gpui_tests {
use gpui::{AppContext, Entity, TestAppContext, VisualTestContext};
fn harness(cx: &mut TestAppContext) -> (Entity<Tty7App>, VisualTestContext) {
// Every keybinding edit below goes through `update_config`, which ends in
// `Config::save()` — a *full* overwrite of `config.json` at whatever path
// the config dir resolves to. Unpinned, that is the developer's real
// `~/.config/tty7/config.json`, so running these tests silently reset the
// user's entire config to `Config::default()` plus the shortcut recorded
// here. Pin a scratch dir first, like every other test module that
// touches config-dir files (`set_config_dir` is first-call-wins, so this
// is a no-op when another test in the same process already pinned one —
// also a scratch dir, so the real file stays untouched either way).
let dir = std::env::temp_dir().join(format!("tty7-kbtest-{}", std::process::id()));
std::fs::create_dir_all(&dir).ok();
crate::core::config::set_config_dir(dir);
// The pause-to-commit is a real `smol::Timer` (off the deterministic
// executor), so waiting on it parks the test thread.
cx.executor().allow_parking();
+1133 -315
View File
File diff suppressed because it is too large Load Diff