Skip to content

Commit 58af0bc

Browse files
committed
initial conduit integration with old loop
1 parent a7ce9c9 commit 58af0bc

File tree

7 files changed

+328
-303
lines changed

7 files changed

+328
-303
lines changed

Cargo.lock

Lines changed: 23 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ schemars = "1.0.4"
131131
jsonschema = "0.30.0"
132132
zip = "2.2.0"
133133
rmcp = { version = "0.7.0", features = ["client", "transport-sse-client-reqwest", "reqwest", "transport-streamable-http-client-reqwest", "transport-child-process", "tower", "auth"] }
134+
chat-cli-ui = { path = "crates/chat-cli-ui" }
134135

135136
[workspace.lints.rust]
136137
future_incompatible = "warn"

crates/chat-cli-ui/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[package]
2-
name = "chat_cli_ui"
2+
name = "chat-cli-ui"
33
authors.workspace = true
44
edition.workspace = true
55
homepage.workspace = true

crates/chat-cli-ui/src/conduit.rs

Lines changed: 139 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use crossterm::{
2-
execute,
3-
style,
4-
};
1+
use std::{io::Write as _, marker::PhantomData};
52

6-
use crate::protocol::Event;
3+
use crate::protocol::{Event, LegacyPassThroughOutput};
74

85
#[derive(thiserror::Error, Debug)]
96
pub enum ConduitError {
@@ -33,40 +30,63 @@ impl ViewEnd {
3330
/// Method to facilitate in the interim
3431
/// It takes possible messages from the old even loop and queues write to the output provided
3532
/// This blocks the current thread and consumes the [ViewEnd]
36-
pub fn into_legacy_mode(self, mut output: impl std::io::Write) -> Result<(), ConduitError> {
33+
pub fn into_legacy_mode(
34+
self,
35+
mut stderr: std::io::Stderr,
36+
mut stdout: std::io::Stdout,
37+
) -> Result<(), ConduitError> {
3738
while let Ok(event) = self.receiver.recv() {
38-
let content = match event {
39-
Event::Custom(custom) => custom.value.to_string(),
40-
Event::TextMessageContent(content) => content.delta,
41-
Event::TextMessageChunk(chunk) => {
42-
if let Some(content) = chunk.delta {
43-
content
44-
} else {
45-
continue;
46-
}
47-
},
48-
_ => continue,
49-
};
50-
51-
execute!(&mut output, style::Print(content))?;
39+
if let Event::LegacyPassThrough(content) = event {
40+
match content {
41+
LegacyPassThroughOutput::Stderr(content) => {
42+
stderr.write_all(&content)?;
43+
stderr.flush()?;
44+
},
45+
LegacyPassThroughOutput::Stdout(content) => {
46+
stdout.write_all(&content)?;
47+
stdout.flush()?;
48+
},
49+
}
50+
}
5251
}
5352

5453
Ok(())
5554
}
5655
}
5756

57+
#[derive(Clone, Debug)]
58+
pub struct DestinationStdout;
59+
#[derive(Clone, Debug)]
60+
pub struct DestinationStderr;
61+
#[derive(Clone, Debug)]
62+
pub struct DestinationStructuredOutput;
63+
64+
pub type InputReceiver = std::sync::mpsc::Receiver<Vec<u8>>;
65+
5866
/// This compliments the [ViewEnd]. It can be thought of as the "other end" of a pipe.
5967
/// The control would own this.
60-
pub struct ControlEnd {
68+
#[derive(Debug)]
69+
pub struct ControlEnd<T> {
6170
pub current_event: Option<Event>,
6271
/// Used by the control to send state changes to the view
6372
pub sender: std::sync::mpsc::Sender<Event>,
64-
/// To receive user input from the view
65-
// TODO: later on we will need replace this byte array with an actual event type from ACP
66-
pub receiver: std::sync::mpsc::Receiver<Vec<u8>>,
73+
/// Phantom data to specify the destination type for pass-through operations.
74+
/// This allows the type system to track whether this ControlEnd is configured
75+
/// for stdout or stderr output without runtime overhead.
76+
pass_through_destination: PhantomData<T>,
6777
}
6878

69-
impl ControlEnd {
79+
impl<T> Clone for ControlEnd<T> {
80+
fn clone(&self) -> Self {
81+
Self {
82+
current_event: self.current_event.clone(),
83+
sender: self.sender.clone(),
84+
pass_through_destination: PhantomData,
85+
}
86+
}
87+
}
88+
89+
impl<T> ControlEnd<T> {
7090
/// Primes the [ControlEnd] with the state passed in
7191
/// This api is intended to serve as an interim solution to bridge the gap between the current
7292
/// code base, which heavily relies on crossterm apis to print directly to the terminal and the
@@ -81,13 +101,33 @@ impl ControlEnd {
81101
}
82102
}
83103

84-
impl std::io::Write for ControlEnd {
104+
impl ControlEnd<DestinationStderr> {
105+
pub fn as_stdout(&self) -> ControlEnd<DestinationStdout> {
106+
ControlEnd {
107+
current_event: self.current_event.clone(),
108+
sender: self.sender.clone(),
109+
pass_through_destination: PhantomData,
110+
}
111+
}
112+
}
113+
114+
impl ControlEnd<DestinationStdout> {
115+
pub fn as_stderr(&self) -> ControlEnd<DestinationStderr> {
116+
ControlEnd {
117+
current_event: self.current_event.clone(),
118+
sender: self.sender.clone(),
119+
pass_through_destination: PhantomData,
120+
}
121+
}
122+
}
123+
124+
impl std::io::Write for ControlEnd<DestinationStderr> {
85125
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
86-
// We'll default to custom event
87-
// This hardly matters because in legacy mode we are simply extracting the bytes and
88-
// dumping it to output
89126
if self.current_event.is_none() {
90-
self.current_event.replace(Event::Custom(Default::default()));
127+
self.current_event
128+
.replace(Event::LegacyPassThrough(LegacyPassThroughOutput::Stderr(
129+
Default::default(),
130+
)));
91131
}
92132

93133
let current_event = self
@@ -103,26 +143,74 @@ impl std::io::Write for ControlEnd {
103143
}
104144

105145
fn flush(&mut self) -> std::io::Result<()> {
106-
let current_state = self.current_event.take().ok_or(std::io::Error::other("No state set"))?;
146+
let current_state =
147+
self.current_event
148+
.take()
149+
.unwrap_or(Event::LegacyPassThrough(LegacyPassThroughOutput::Stderr(
150+
Default::default(),
151+
)));
107152

108153
self.sender.send(current_state).map_err(std::io::Error::other)
109154
}
110155
}
111156

112-
/// Creates a bidirectional communication channel between view and control layers.
157+
impl std::io::Write for ControlEnd<DestinationStdout> {
158+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
159+
if self.current_event.is_none() {
160+
self.current_event
161+
.replace(Event::LegacyPassThrough(LegacyPassThroughOutput::Stdout(
162+
Default::default(),
163+
)));
164+
}
165+
166+
let current_event = self
167+
.current_event
168+
.as_mut()
169+
.ok_or(std::io::Error::other("No event set"))?;
170+
171+
current_event
172+
.insert_content(buf)
173+
.map_err(|_e| std::io::Error::other("Error inserting content"))?;
174+
175+
Ok(buf.len())
176+
}
177+
178+
fn flush(&mut self) -> std::io::Result<()> {
179+
let current_state =
180+
self.current_event
181+
.take()
182+
.unwrap_or(Event::LegacyPassThrough(LegacyPassThroughOutput::Stdout(
183+
Default::default(),
184+
)));
185+
186+
self.sender.send(current_state).map_err(std::io::Error::other)
187+
}
188+
}
189+
190+
/// Creates a set of legacy conduits for communication between view and control layers.
113191
///
114-
/// This function establishes a message-passing conduit that enables:
115-
/// - The view layer to send user input (as bytes) to the control layer
116-
/// - The control layer to send state changes to the view layer
192+
/// This function establishes the communication channels needed for the legacy mode operation,
193+
/// where the view layer and control layer can exchange events and byte data.
117194
///
118195
/// # Returns
196+
///
119197
/// A tuple containing:
120-
/// - `ViewEnd<S>`: The view-side endpoint for sending input and receiving state updates
121-
/// - `ControlEnd<S>`: The control-side endpoint for receiving input and sending state updates
198+
/// - `ViewEnd`: The view-side endpoint for sending input and receiving state changes
199+
/// - `InputReceiver`: A receiver for raw byte input from the view
200+
/// - `ControlEnd<DestinationStderr>`: Control endpoint configured for stderr output
201+
/// - `ControlEnd<DestinationStdout>`: Control endpoint configured for stdout output
202+
///
203+
/// # Example
122204
///
123-
/// # Type Parameters
124-
/// - `S`: The state type that implements `ViewState` trait
125-
pub fn get_conduit_pair() -> (ViewEnd, ControlEnd) {
205+
/// ```rust
206+
/// let (view_end, input_receiver, stderr_control, stdout_control) = get_legacy_conduits();
207+
/// ```
208+
pub fn get_legacy_conduits() -> (
209+
ViewEnd,
210+
InputReceiver,
211+
ControlEnd<DestinationStderr>,
212+
ControlEnd<DestinationStdout>,
213+
) {
126214
let (state_tx, state_rx) = std::sync::mpsc::channel::<Event>();
127215
let (byte_tx, byte_rx) = std::sync::mpsc::channel::<Vec<u8>>();
128216

@@ -131,10 +219,16 @@ pub fn get_conduit_pair() -> (ViewEnd, ControlEnd) {
131219
sender: byte_tx,
132220
receiver: state_rx,
133221
},
222+
byte_rx,
223+
ControlEnd {
224+
current_event: None,
225+
sender: state_tx.clone(),
226+
pass_through_destination: PhantomData,
227+
},
134228
ControlEnd {
135229
current_event: None,
136230
sender: state_tx,
137-
receiver: byte_rx,
231+
pass_through_destination: PhantomData,
138232
},
139233
)
140234
}
@@ -154,22 +248,10 @@ impl InterimEvent for Event {
154248
debug_assert!(self.is_compatible_with_legacy_event_loop());
155249

156250
match self {
157-
Self::Custom(_custom) => {
158-
// custom events are defined in this UI crate
159-
// TODO: use an enum as implement AsRef for it
160-
// match custom.name.as_str() {
161-
// _ => {},
162-
// }
163-
},
164-
Self::TextMessageContent(msg_content) => {
165-
let str = String::from_utf8(content.to_vec())?;
166-
msg_content.delta.push_str(&str);
167-
},
168-
Self::TextMessageChunk(chunk) => {
169-
let str = String::from_utf8(content.to_vec())?;
170-
if let Some(d) = chunk.delta.as_mut() {
171-
d.push_str(&str);
172-
}
251+
Self::LegacyPassThrough(buf) => match buf {
252+
LegacyPassThroughOutput::Stdout(buf) | LegacyPassThroughOutput::Stderr(buf) => {
253+
buf.extend_from_slice(content);
254+
},
173255
},
174256
_ => unreachable!(),
175257
}

0 commit comments

Comments
 (0)