From a39bce779ce6db59063abf05554c4be261e8be9b Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 21 Apr 2026 23:59:49 +0900 Subject: [PATCH] =?UTF-8?q?=E8=A4=87=E6=95=B0=E3=82=AF=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=A2=E3=83=B3=E3=83=88=E9=96=93=E3=81=A7=E3=81=AERun?= =?UTF-8?q?=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=81=AE=E5=90=8C=E6=9C=9F?= =?UTF-8?q?=E6=BC=8F=E3=82=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/pod/src/controller.rs | 7 +++++++ crates/protocol/src/lib.rs | 29 +++++++++++++++++++++++++++++ crates/tui/src/app.rs | 17 ++++++++++++----- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/pod/src/controller.rs b/crates/pod/src/controller.rs index 07596f90..929a690f 100644 --- a/crates/pod/src/controller.rs +++ b/crates/pod/src/controller.rs @@ -279,6 +279,13 @@ impl PodController { }); continue; } + // Broadcast the accepted user message so every + // subscriber (including the submitter) can + // render the turn header + user line from a + // single source of truth. + let _ = event_tx.send(Event::UserMessage { + text: input.clone(), + }); let was_paused = status_before == PodStatus::Paused; shared_state.set_status(PodStatus::Running); let _ = runtime_dir.write_status(&shared_state).await; diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index cf7d0a43..a5957327 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -83,6 +83,18 @@ pub enum PodEvent { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "event", content = "data", rename_all = "snake_case")] pub enum Event { + /// A user input message was accepted by the Pod and is about to + /// start a new turn. Broadcast to every subscribed client so + /// additional TUI / GUI instances show the same pending user line + /// that the submitter already sees — without this event, non- + /// submitting clients would see tool calls and assistant text + /// appear without any preceding user message. + /// + /// Fires exactly once per accepted `Method::Run`, before + /// `TurnStart`. Rejected runs (e.g. `AlreadyRunning`) do not emit. + UserMessage { + text: String, + }, TurnStart { turn: usize, }, @@ -596,4 +608,21 @@ mod tests { assert_eq!(parsed["event"], "error"); assert_eq!(parsed["data"]["code"], "already_running"); } + + #[test] + fn event_user_message_roundtrip() { + let event = Event::UserMessage { + text: "hello 世界".into(), + }; + let json = serde_json::to_string(&event).unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed["event"], "user_message"); + assert_eq!(parsed["data"]["text"], "hello 世界"); + + let decoded: Event = serde_json::from_str(&json).unwrap(); + match decoded { + Event::UserMessage { text } => assert_eq!(text, "hello 世界"), + other => panic!("expected UserMessage, got {other:?}"), + } + } } diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 85fea3c6..7e5a075d 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -72,11 +72,10 @@ impl App { } return None; } - self.turn_index += 1; - self.blocks.push(Block::TurnHeader { - turn: self.turn_index, - }); - self.blocks.push(Block::UserMessage { text: text.clone() }); + // TurnHeader / UserMessage blocks are pushed in response to + // `Event::UserMessage` (single source of truth, shared by every + // client subscribed to the Pod). Locally we only clear the + // input buffer and forward the method. self.input.clear(); Some(Method::Run { input: text }) } @@ -91,6 +90,14 @@ impl App { pub fn handle_pod_event(&mut self, event: Event) { match event { + Event::UserMessage { text } => { + self.turn_index += 1; + self.blocks.push(Block::TurnHeader { + turn: self.turn_index, + }); + self.blocks.push(Block::UserMessage { text }); + self.assistant_streaming = false; + } Event::TurnStart { .. } => { self.running = true; self.paused = false;