fix(ui): preserve sidebar scroll across clients (#2280)
refs #2255 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
This commit is contained in:
parent
adb50cba9b
commit
e6fa5289f2
|
|
@ -6,6 +6,7 @@
|
|||
- The plugin marketplace now discovers valid manifests at repository roots and subdirectories, groups multiple plugins under each repository, and publishes their versions and exact default-branch commits.
|
||||
|
||||
### Fixed
|
||||
- Sidebar agent lists keep scrolling when differently sized clients are attached to the same session. (#2255, thanks @aiworkflowpro)
|
||||
- `pane send-keys` and `agent send-keys` now preserve Shift when sending `shift+tab`, allowing agent permission modes to be cycled programmatically. (#1561, thanks @keinstn and @tomohisa)
|
||||
|
||||
## [0.8.0] - 2026-08-03
|
||||
|
|
|
|||
|
|
@ -4072,6 +4072,12 @@ impl HeadlessServer {
|
|||
} else {
|
||||
crate::kitty_graphics::HostCellSize::default()
|
||||
};
|
||||
let preserved_scroll = (!is_foreground).then_some((
|
||||
self.app.state.workspace_scroll,
|
||||
self.app.state.agent_panel_scroll,
|
||||
self.app.state.tab_scroll,
|
||||
self.app.state.mobile_switcher_scroll,
|
||||
));
|
||||
let (buffer, cursor) =
|
||||
crate::server::render_stream::render_virtual_with_runtime_registry(
|
||||
&mut self.app.state,
|
||||
|
|
@ -4080,6 +4086,12 @@ impl HeadlessServer {
|
|||
is_foreground,
|
||||
render_cell_size,
|
||||
);
|
||||
if let Some((workspace, agent_panel, tab, mobile_switcher)) = preserved_scroll {
|
||||
self.app.state.workspace_scroll = workspace;
|
||||
self.app.state.agent_panel_scroll = agent_panel;
|
||||
self.app.state.tab_scroll = tab;
|
||||
self.app.state.mobile_switcher_scroll = mobile_switcher;
|
||||
}
|
||||
crate::render_prof::duration_since(
|
||||
"full_render.render_virtual",
|
||||
render_started,
|
||||
|
|
|
|||
|
|
@ -273,6 +273,19 @@ fn create_workspace_and_root_pane(socket_path: &Path, label: &str) -> (String, S
|
|||
(workspace_id, pane_id)
|
||||
}
|
||||
|
||||
fn report_idle_agent(socket_path: &Path, pane_id: &str) {
|
||||
let response = send_json_request(
|
||||
socket_path,
|
||||
&format!(
|
||||
r#"{{"id":"report_agent","method":"pane.report_agent","params":{{"pane_id":"{pane_id}","agent":"pi","state":"idle","source":"multi-client-test"}}}}"#
|
||||
),
|
||||
);
|
||||
assert!(
|
||||
response.get("error").is_none(),
|
||||
"pane.report_agent should succeed: {response}"
|
||||
);
|
||||
}
|
||||
|
||||
fn pane_send_input(socket_path: &Path, pane_id: &str, text: &str) {
|
||||
let request = format!(
|
||||
"{{\"id\":\"send_input\",\"method\":\"pane.send_input\",\"params\":{{\"pane_id\":\"{pane_id}\",\"text\":\"{}\",\"keys\":[\"Enter\"]}}}}",
|
||||
|
|
@ -747,6 +760,15 @@ fn frame_contains_text(frame: &FrameWire, needle: &str) -> bool {
|
|||
frame_text(frame).contains(needle)
|
||||
}
|
||||
|
||||
fn agent_panel_starts_with(frame: &FrameWire, agent_label: &str) -> bool {
|
||||
frame_text(frame)
|
||||
.lines()
|
||||
.skip_while(|line| !line.contains("agents"))
|
||||
.skip(1)
|
||||
.find(|line| line.contains("agent-"))
|
||||
.is_some_and(|line| line.contains(agent_label))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_client_allows_multiple_simultaneous_connections() {
|
||||
let _lock = test_lock();
|
||||
|
|
@ -827,6 +849,75 @@ fn multi_client_effective_size_shrinks_when_smaller_client_joins() {
|
|||
cleanup_spawned_herdr(server, base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_foreground_client_render_preserves_agent_panel_scroll() {
|
||||
let _lock = test_lock();
|
||||
let base = unique_test_dir();
|
||||
let config_home = base.join("config");
|
||||
let runtime_dir = base.join("runtime");
|
||||
let api_socket = runtime_dir.join("herdr.sock");
|
||||
let client_socket = runtime_dir.join("herdr-client.sock");
|
||||
|
||||
let server = spawn_server(&config_home, &runtime_dir, &api_socket);
|
||||
wait_for_socket(&api_socket, Duration::from_secs(10));
|
||||
wait_for_file(&client_socket, Duration::from_secs(10));
|
||||
|
||||
for index in 1..=23 {
|
||||
let (_, pane_id) =
|
||||
create_workspace_and_root_pane(&api_socket, &format!("agent-{index:02}"));
|
||||
report_idle_agent(&api_socket, &pane_id);
|
||||
}
|
||||
|
||||
let mut setup_client = connect_raw_client(&client_socket, 106, 40);
|
||||
assert!(wait_for_frame(&mut setup_client, Duration::from_secs(2)));
|
||||
drain_server_messages(&mut setup_client, Duration::from_millis(250));
|
||||
|
||||
let wheel_down = b"\x1b[<65;10;30M";
|
||||
send_client_input(&mut setup_client, &wheel_down.repeat(20));
|
||||
let (reached_bottom, setup_frames) = wait_for_frame_matching_with_snapshots(
|
||||
&mut setup_client,
|
||||
Duration::from_secs(3),
|
||||
|frame| agent_panel_starts_with(frame, "agent-16"),
|
||||
)
|
||||
.expect("setup frame decoding should succeed");
|
||||
assert!(
|
||||
reached_bottom,
|
||||
"40-row client should scroll the agent panel to its final page; frames:\n{}",
|
||||
setup_frames.join("\n--- frame ---\n")
|
||||
);
|
||||
send_client_detach(&mut setup_client);
|
||||
drop(setup_client);
|
||||
|
||||
let mut tall_background = connect_raw_client(&client_socket, 106, 64);
|
||||
assert!(wait_for_frame(&mut tall_background, Duration::from_secs(2)));
|
||||
let mut probe = connect_raw_client(&client_socket, 106, 40);
|
||||
let (started_at_tall_limit, initial_frames) =
|
||||
wait_for_frame_matching_with_snapshots(&mut probe, Duration::from_secs(3), |frame| {
|
||||
agent_panel_starts_with(frame, "agent-10")
|
||||
})
|
||||
.expect("initial probe frame decoding should succeed");
|
||||
assert!(
|
||||
started_at_tall_limit,
|
||||
"tall client should normalize the shared scroll before the probe attaches; frames:\n{}",
|
||||
initial_frames.join("\n--- frame ---\n")
|
||||
);
|
||||
drain_server_messages(&mut probe, Duration::from_millis(250));
|
||||
|
||||
send_client_input(&mut probe, wheel_down);
|
||||
let (scrolled, probe_frames) =
|
||||
wait_for_frame_matching_with_snapshots(&mut probe, Duration::from_secs(3), |frame| {
|
||||
agent_panel_starts_with(frame, "agent-11")
|
||||
})
|
||||
.expect("probe frame decoding should succeed");
|
||||
assert!(
|
||||
scrolled,
|
||||
"background client projection must not undo the foreground wheel event; frames:\n{}",
|
||||
probe_frames.join("\n--- frame ---\n")
|
||||
);
|
||||
|
||||
cleanup_spawned_herdr(server, base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_client_broadcasts_frame_updates_to_all_clients() {
|
||||
let _lock = test_lock();
|
||||
|
|
|
|||
Loading…
Reference in New Issue