fix(domain,infrastructure,web-server,app-tauri,frontend): TUI native noire — route PTY explicite et replay du scrollback pré-attach sans perte — #360 (QA verte)

This commit is contained in:
2026-09-16 19:08:01 +02:00
parent a833af0fb5
commit 355a482fee
8 changed files with 220 additions and 13 deletions

View File

@ -1565,7 +1565,7 @@ async fn ws_open_terminal(
code: "WS_CLOSED".to_owned(),
message: "websocket output closed".to_owned(),
})?;
attach_sink_and_pump(state, sid, sink, owned)
attach_sink_and_pump(state, sid, sink, owned, true)
}
async fn ws_launch_agent(
@ -1626,7 +1626,7 @@ async fn send_launch_agent_attached(
code: "WS_CLOSED".to_owned(),
message: "websocket output closed".to_owned(),
})?;
attach_sink_and_pump(state, session_id, sink, owned)
attach_sink_and_pump(state, session_id, sink, owned, true)
}
async fn execute_launch_agent_for_ws(
@ -1764,7 +1764,7 @@ async fn ws_attach_terminal(
message: "websocket output closed".to_owned(),
})?;
let sink = WsPtySink::new(sid, tx.clone(), next_seq);
attach_sink_and_pump(state, sid, sink, owned)
attach_sink_and_pump(state, sid, sink, owned, false)
}
fn attach_sink_and_pump(
@ -1772,17 +1772,27 @@ fn attach_sink_and_pump(
sid: SessionId,
sink: WsPtySink,
owned: &Arc<Mutex<Vec<WsAttachment>>>,
replay_initial_scrollback: bool,
) -> Result<(), ErrorDto> {
let handle = PtyHandle { session_id: sid };
let (scrollback, stream) = if replay_initial_scrollback {
state.app.pty_port.subscribe_output_with_scrollback(&handle)
} else {
state
.app
.pty_port
.subscribe_output(&handle)
.map(|stream| (Vec::new(), stream))
}
.map_err(|err| ErrorDto::from(application::AppError::from(err)))?;
let gen = state.ws_pty_bridge.register(sid, Arc::new(sink));
if let Ok(mut owned) = owned.lock() {
owned.push(WsAttachment::Pty { session: sid, gen });
}
let handle = PtyHandle { session_id: sid };
let stream = state
.app
.pty_port
.subscribe_output(&handle)
.map_err(|err| ErrorDto::from(application::AppError::from(err)))?;
if !scrollback.is_empty() {
let _ = state.ws_pty_bridge.send_output(&sid, scrollback);
}
let bridge = Arc::clone(&state.ws_pty_bridge);
std::thread::spawn(move || {
for chunk in stream {
@ -8112,6 +8122,40 @@ mod tests {
close_ws_session(&state, &session_id).await;
}
#[tokio::test]
async fn websocket_open_terminal_delivers_the_initial_process_output() {
let state = state();
let (tx, mut rx) = mpsc::channel(16);
let owned = Arc::new(Mutex::new(Vec::new()));
handle_client_frame(
ws_frame(
"open-initial-output",
"terminal.open",
open_terminal_payload("/bin/sh", vec!["-c", "printf initial-screen; sleep 30"]),
),
&state,
&tx,
&owned,
)
.await;
let attached = recv_server_frame(&mut rx).await;
let session_id = attached_session_id(&attached);
let output = recv_server_frame(&mut rx).await;
assert_eq!(attached.kind, "terminal.attached");
assert_eq!(output.kind, "terminal.output");
assert_eq!(output.payload["sessionId"], session_id);
assert_eq!(output.payload["seq"], 0);
assert_eq!(
base64_decode(output.payload["bytesBase64"].as_str().unwrap()).unwrap(),
b"initial-screen"
);
close_ws_session(&state, &session_id).await;
}
#[tokio::test]
async fn websocket_attach_terminal_replays_scrollback_in_attached_ack() {
let state = state();