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:
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user