Skip to content

Commit 7db7092

Browse files
committed
initial conduit integration with old loop
1 parent a7ce9c9 commit 7db7092

File tree

7 files changed

+256
-298
lines changed

7 files changed

+256
-298
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: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use crossterm::{
2-
execute,
3-
style,
4-
};
1+
use crossterm::{execute, style};
52

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

85
#[derive(thiserror::Error, Debug)]
96
pub enum ConduitError {
@@ -33,37 +30,58 @@ 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+
let content_as_str = String::from_utf8(content)?;
43+
execute!(&mut stderr, style::Print(content_as_str))?;
44+
},
45+
LegacyPassThroughOutput::Stdout(content) => {
46+
let content_as_str = String::from_utf8(content)?;
47+
execute!(&mut stdout, style::Print(content_as_str))?;
48+
},
49+
}
50+
}
5251
}
5352

5453
Ok(())
5554
}
5655
}
5756

57+
#[derive(Clone, Debug)]
58+
pub enum TargetOutput {
59+
Stdout,
60+
Stderr,
61+
}
62+
5863
/// This compliments the [ViewEnd]. It can be thought of as the "other end" of a pipe.
5964
/// The control would own this.
65+
#[derive(Debug)]
6066
pub struct ControlEnd {
6167
pub current_event: Option<Event>,
6268
/// Used by the control to send state changes to the view
6369
pub sender: std::sync::mpsc::Sender<Event>,
6470
/// To receive user input from the view
6571
// 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>>,
72+
pub receiver: Option<std::sync::mpsc::Receiver<Vec<u8>>>,
73+
pub target_output: TargetOutput,
74+
}
75+
76+
impl Clone for ControlEnd {
77+
fn clone(&self) -> Self {
78+
Self {
79+
current_event: self.current_event.clone(),
80+
sender: self.sender.clone(),
81+
receiver: None,
82+
target_output: self.target_output.clone(),
83+
}
84+
}
6785
}
6886

6987
impl ControlEnd {
@@ -79,15 +97,19 @@ impl ControlEnd {
7997
pub fn send(&self, event: Event) -> Result<(), ConduitError> {
8098
Ok(self.sender.send(event).map_err(Box::new)?)
8199
}
100+
101+
pub fn as_stdout(&self) -> Self {
102+
let mut self_as_stdout = self.clone();
103+
self_as_stdout.target_output = TargetOutput::Stdout;
104+
105+
self_as_stdout
106+
}
82107
}
83108

84109
impl std::io::Write for ControlEnd {
85110
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
89111
if self.current_event.is_none() {
90-
self.current_event.replace(Event::Custom(Default::default()));
112+
self.current_event.replace(Event::LegacyPassThrough(Default::default()));
91113
}
92114

93115
let current_event = self
@@ -117,11 +139,8 @@ impl std::io::Write for ControlEnd {
117139
///
118140
/// # Returns
119141
/// 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
122-
///
123-
/// # Type Parameters
124-
/// - `S`: The state type that implements `ViewState` trait
142+
/// - `ViewEnd`: The view-side endpoint for sending input and receiving state updates
143+
/// - `ControlEnd`: The control-side endpoint for receiving input and sending state updates
125144
pub fn get_conduit_pair() -> (ViewEnd, ControlEnd) {
126145
let (state_tx, state_rx) = std::sync::mpsc::channel::<Event>();
127146
let (byte_tx, byte_rx) = std::sync::mpsc::channel::<Vec<u8>>();
@@ -134,7 +153,8 @@ pub fn get_conduit_pair() -> (ViewEnd, ControlEnd) {
134153
ControlEnd {
135154
current_event: None,
136155
sender: state_tx,
137-
receiver: byte_rx,
156+
receiver: Some(byte_rx),
157+
target_output: TargetOutput::Stderr,
138158
},
139159
)
140160
}
@@ -154,22 +174,10 @@ impl InterimEvent for Event {
154174
debug_assert!(self.is_compatible_with_legacy_event_loop());
155175

156176
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-
}
177+
Self::LegacyPassThrough(buf) => match buf {
178+
LegacyPassThroughOutput::Stdout(buf) | LegacyPassThroughOutput::Stderr(buf) => {
179+
buf.extend_from_slice(content);
180+
},
173181
},
174182
_ => unreachable!(),
175183
}

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
//! This is largely based on https://docs.ag-ui.com/concepts/events
22
//! They do not have a rust SDK so for now we are handrolling these types
33
4-
use serde::{
5-
Deserialize,
6-
Serialize,
7-
};
4+
use serde::{Deserialize, Serialize};
85
use serde_json::Value;
96

107
/// Role of a message sender
@@ -97,7 +94,7 @@ pub struct TextMessageStart {
9794
}
9895

9996
/// Represents a chunk of content in a streaming text message
100-
#[derive(Debug, Clone, Serialize, Deserialize)]
97+
#[derive(Debug, Clone, Serialize, Default, Deserialize)]
10198
#[serde(rename_all = "camelCase")]
10299
pub struct TextMessageContent {
103100
pub message_id: String,
@@ -214,6 +211,24 @@ pub struct Custom {
214211
pub value: Value,
215212
}
216213

214+
/// Legacy pass-through output for compatibility with older event systems.
215+
///
216+
/// This enum represents different types of output that can be passed through
217+
/// from legacy systems that haven't been fully migrated to the new event protocol.
218+
#[derive(Debug, Clone, Serialize, Deserialize)]
219+
pub enum LegacyPassThroughOutput {
220+
/// Standard output stream data
221+
Stdout(Vec<u8>),
222+
/// Standard error stream data
223+
Stderr(Vec<u8>),
224+
}
225+
226+
impl Default for LegacyPassThroughOutput {
227+
fn default() -> Self {
228+
Self::Stderr(Default::default())
229+
}
230+
}
231+
217232
// ============================================================================
218233
// Draft Events - Activity Events
219234
// ============================================================================
@@ -336,6 +351,7 @@ pub enum Event {
336351
// Special Events
337352
Raw(Raw),
338353
Custom(Custom),
354+
LegacyPassThrough(LegacyPassThroughOutput),
339355

340356
// Draft Events - Activity Events
341357
ActivitySnapshotEvent(ActivitySnapshotEvent),
@@ -384,6 +400,7 @@ impl Event {
384400
// Special Events
385401
Event::Raw(_) => "raw",
386402
Event::Custom(_) => "custom",
403+
Event::LegacyPassThrough(_) => "legacyPassThrough",
387404

388405
// Draft Events - Activity Events
389406
Event::ActivitySnapshotEvent(_) => "activitySnapshotEvent",
@@ -403,10 +420,7 @@ impl Event {
403420
}
404421

405422
pub fn is_compatible_with_legacy_event_loop(&self) -> bool {
406-
matches!(
407-
self,
408-
Event::Custom(_) | Event::TextMessageContent(_) | Event::TextMessageChunk(_)
409-
)
423+
matches!(self, Event::LegacyPassThrough(_))
410424
}
411425

412426
/// Check if this is a lifecycle event

crates/chat-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ schemars.workspace = true
119119
jsonschema.workspace = true
120120
zip.workspace = true
121121
rmcp.workspace = true
122+
chat-cli-ui.workspace = true
122123

123124
[target.'cfg(unix)'.dependencies]
124125
nix.workspace = true

0 commit comments

Comments
 (0)