Move TUI on top of app server (parallel code) (#14717)

This PR replicates the `tui` code directory and creates a temporary
parallel `tui_app_server` directory. It also implements a new feature
flag `tui_app_server` to select between the two tui implementations.

Once the new app-server-based TUI is stabilized, we'll delete the old
`tui` directory and feature flag.
This commit is contained in:
Eric Traut 2026-03-16 10:49:19 -06:00 committed by GitHub
parent c04a0a7454
commit db89b73a9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1109 changed files with 134253 additions and 17 deletions

View file

@ -6,3 +6,4 @@ MODULE.bazel.lock
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
codex-rs/tui/tests/fixtures/oss-story.jsonl
codex-rs/tui_app_server/tests/fixtures/oss-story.jsonl

View file

@ -54,6 +54,8 @@ See `codex-rs/tui/styles.md`.
## TUI code conventions
- When a change lands in `codex-rs/tui` and `codex-rs/tui_app_server` has a parallel implementation of the same behavior, reflect the change in `codex-rs/tui_app_server` too unless there is a documented reason not to.
- Use concise styling helpers from ratatuis Stylize trait.
- Basic spans: use "text".into()
- Styled spans: use "text".red(), "text".green(), "text".magenta(), "text".dim(), etc.

95
codex-rs/Cargo.lock generated
View file

@ -1476,12 +1476,15 @@ dependencies = [
"codex-core",
"codex-feedback",
"codex-protocol",
"futures",
"pretty_assertions",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite",
"toml 0.9.11+spec-1.1.0",
"tracing",
"url",
]
[[package]]
@ -1660,6 +1663,7 @@ dependencies = [
"codex-state",
"codex-stdio-to-uds",
"codex-tui",
"codex-tui-app-server",
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-windows-sandbox",
@ -2512,6 +2516,97 @@ dependencies = [
"codex-protocol",
"codex-shell-command",
"codex-state",
"codex-tui-app-server",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
"codex-utils-cargo-bin",
"codex-utils-cli",
"codex-utils-elapsed",
"codex-utils-fuzzy-match",
"codex-utils-oss",
"codex-utils-pty",
"codex-utils-sandbox-summary",
"codex-utils-sleep-inhibitor",
"codex-utils-string",
"codex-windows-sandbox",
"color-eyre",
"cpal",
"crossterm",
"derive_more 2.1.1",
"diffy",
"dirs",
"dunce",
"hound",
"image",
"insta",
"itertools 0.14.0",
"lazy_static",
"libc",
"pathdiff",
"pretty_assertions",
"pulldown-cmark",
"rand 0.9.2",
"ratatui",
"ratatui-macros",
"regex-lite",
"reqwest",
"rmcp",
"serde",
"serde_json",
"serial_test",
"shlex",
"strum 0.27.2",
"strum_macros 0.28.0",
"supports-color 3.0.2",
"syntect",
"tempfile",
"textwrap 0.16.2",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tokio-util",
"toml 0.9.11+spec-1.1.0",
"tracing",
"tracing-appender",
"tracing-subscriber",
"two-face",
"unicode-segmentation",
"unicode-width 0.2.1",
"url",
"uuid",
"vt100",
"webbrowser",
"which",
"windows-sys 0.52.0",
"winsplit",
]
[[package]]
name = "codex-tui-app-server"
version = "0.0.0"
dependencies = [
"anyhow",
"arboard",
"assert_matches",
"base64 0.22.1",
"chrono",
"clap",
"codex-ansi-escape",
"codex-app-server-client",
"codex-app-server-protocol",
"codex-arg0",
"codex-chatgpt",
"codex-cli",
"codex-client",
"codex-cloud-requirements",
"codex-core",
"codex-feedback",
"codex-file-search",
"codex-login",
"codex-otel",
"codex-protocol",
"codex-shell-command",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-approval-presets",
"codex-utils-cargo-bin",

View file

@ -42,6 +42,7 @@ members = [
"stdio-to-uds",
"otel",
"tui",
"tui_app_server",
"utils/absolute-path",
"utils/cargo-bin",
"utils/git",
@ -129,6 +130,7 @@ codex-state = { path = "state" }
codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-test-macros = { path = "test-macros" }
codex-tui = { path = "tui" }
codex-tui-app-server = { path = "tui_app_server" }
codex-utils-absolute-path = { path = "utils/absolute-path" }
codex-utils-approval-presets = { path = "utils/approval-presets" }
codex-utils-cache = { path = "utils/cache" }

View file

@ -18,11 +18,14 @@ codex-arg0 = { workspace = true }
codex-core = { workspace = true }
codex-feedback = { workspace = true }
codex-protocol = { workspace = true }
futures = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync", "time", "rt"] }
tokio-tungstenite = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }

View file

@ -15,6 +15,8 @@
//! bridging async `mpsc` channels on both sides. Queues are bounded so overload
//! surfaces as channel-full errors rather than unbounded memory growth.
mod remote;
use std::error::Error;
use std::fmt;
use std::io::Error as IoError;
@ -33,8 +35,11 @@ use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::Result as JsonRpcResult;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_arg0::Arg0DispatchPaths;
use codex_core::AuthManager;
use codex_core::ThreadManager;
@ -51,6 +56,9 @@ use tokio::time::timeout;
use toml::Value as TomlValue;
use tracing::warn;
pub use crate::remote::RemoteAppServerClient;
pub use crate::remote::RemoteAppServerConnectArgs;
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
/// Raw app-server request result for typed in-process requests.
@ -60,6 +68,30 @@ const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
/// `MessageProcessor` continues to produce that shape internally.
pub type RequestResult = std::result::Result<JsonRpcResult, JSONRPCErrorError>;
#[derive(Debug, Clone)]
pub enum AppServerEvent {
Lagged { skipped: usize },
ServerNotification(ServerNotification),
LegacyNotification(JSONRPCNotification),
ServerRequest(ServerRequest),
Disconnected { message: String },
}
impl From<InProcessServerEvent> for AppServerEvent {
fn from(value: InProcessServerEvent) -> Self {
match value {
InProcessServerEvent::Lagged { skipped } => Self::Lagged { skipped },
InProcessServerEvent::ServerNotification(notification) => {
Self::ServerNotification(notification)
}
InProcessServerEvent::LegacyNotification(notification) => {
Self::LegacyNotification(notification)
}
InProcessServerEvent::ServerRequest(request) => Self::ServerRequest(request),
}
}
}
fn event_requires_delivery(event: &InProcessServerEvent) -> bool {
// These terminal events drive surface shutdown/completion state. Dropping
// them under backpressure can leave exec/TUI waiting forever even though
@ -281,6 +313,22 @@ pub struct InProcessAppServerClient {
thread_manager: Arc<ThreadManager>,
}
#[derive(Clone)]
pub struct InProcessAppServerRequestHandle {
command_tx: mpsc::Sender<ClientCommand>,
}
#[derive(Clone)]
pub enum AppServerRequestHandle {
InProcess(InProcessAppServerRequestHandle),
Remote(crate::remote::RemoteAppServerRequestHandle),
}
pub enum AppServerClient {
InProcess(InProcessAppServerClient),
Remote(RemoteAppServerClient),
}
impl InProcessAppServerClient {
/// Starts the in-process runtime and facade worker task.
///
@ -457,6 +505,12 @@ impl InProcessAppServerClient {
self.thread_manager.clone()
}
pub fn request_handle(&self) -> InProcessAppServerRequestHandle {
InProcessAppServerRequestHandle {
command_tx: self.command_tx.clone(),
}
}
/// Sends a typed client request and returns raw JSON-RPC result.
///
/// Callers that expect a concrete response type should usually prefer
@ -641,9 +695,141 @@ impl InProcessAppServerClient {
}
}
impl InProcessAppServerRequestHandle {
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
let (response_tx, response_rx) = oneshot::channel();
self.command_tx
.send(ClientCommand::Request {
request: Box::new(request),
response_tx,
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"in-process app-server worker channel is closed",
)
})?;
response_rx.await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"in-process app-server request channel is closed",
)
})?
}
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
where
T: DeserializeOwned,
{
let method = request_method_name(&request);
let response =
self.request(request)
.await
.map_err(|source| TypedRequestError::Transport {
method: method.clone(),
source,
})?;
let result = response.map_err(|source| TypedRequestError::Server {
method: method.clone(),
source,
})?;
serde_json::from_value(result)
.map_err(|source| TypedRequestError::Deserialize { method, source })
}
}
impl AppServerRequestHandle {
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
match self {
Self::InProcess(handle) => handle.request(request).await,
Self::Remote(handle) => handle.request(request).await,
}
}
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
where
T: DeserializeOwned,
{
match self {
Self::InProcess(handle) => handle.request_typed(request).await,
Self::Remote(handle) => handle.request_typed(request).await,
}
}
}
impl AppServerClient {
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
match self {
Self::InProcess(client) => client.request(request).await,
Self::Remote(client) => client.request(request).await,
}
}
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
where
T: DeserializeOwned,
{
match self {
Self::InProcess(client) => client.request_typed(request).await,
Self::Remote(client) => client.request_typed(request).await,
}
}
pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
match self {
Self::InProcess(client) => client.notify(notification).await,
Self::Remote(client) => client.notify(notification).await,
}
}
pub async fn resolve_server_request(
&self,
request_id: RequestId,
result: JsonRpcResult,
) -> IoResult<()> {
match self {
Self::InProcess(client) => client.resolve_server_request(request_id, result).await,
Self::Remote(client) => client.resolve_server_request(request_id, result).await,
}
}
pub async fn reject_server_request(
&self,
request_id: RequestId,
error: JSONRPCErrorError,
) -> IoResult<()> {
match self {
Self::InProcess(client) => client.reject_server_request(request_id, error).await,
Self::Remote(client) => client.reject_server_request(request_id, error).await,
}
}
pub async fn next_event(&mut self) -> Option<AppServerEvent> {
match self {
Self::InProcess(client) => client.next_event().await.map(Into::into),
Self::Remote(client) => client.next_event().await,
}
}
pub async fn shutdown(self) -> IoResult<()> {
match self {
Self::InProcess(client) => client.shutdown().await,
Self::Remote(client) => client.shutdown().await,
}
}
pub fn request_handle(&self) -> AppServerRequestHandle {
match self {
Self::InProcess(client) => AppServerRequestHandle::InProcess(client.request_handle()),
Self::Remote(client) => AppServerRequestHandle::Remote(client.request_handle()),
}
}
}
/// Extracts the JSON-RPC method name for diagnostics without extending the
/// protocol crate with in-process-only helpers.
fn request_method_name(request: &ClientRequest) -> String {
pub(crate) fn request_method_name(request: &ClientRequest) -> String {
serde_json::to_value(request)
.ok()
.and_then(|value| {
@ -658,16 +844,29 @@ fn request_method_name(request: &ClientRequest) -> String {
#[cfg(test)]
mod tests {
use super::*;
use codex_app_server_protocol::AccountUpdatedNotification;
use codex_app_server_protocol::ConfigRequirementsReadResponse;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::SessionSource as ApiSessionSource;
use codex_app_server_protocol::ThreadStartParams;
use codex_app_server_protocol::ThreadStartResponse;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_app_server_protocol::ToolRequestUserInputQuestion;
use codex_core::AuthManager;
use codex_core::ThreadManager;
use codex_core::config::ConfigBuilder;
use futures::SinkExt;
use futures::StreamExt;
use pretty_assertions::assert_eq;
use tokio::net::TcpListener;
use tokio::time::Duration;
use tokio::time::timeout;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::tungstenite::Message;
async fn build_test_config() -> Config {
match ConfigBuilder::default().build().await {
@ -705,6 +904,97 @@ mod tests {
start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await
}
async fn start_test_remote_server<F, Fut>(handler: F) -> String
where
F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut
+ Send
+ 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
let addr = listener.local_addr().expect("listener address");
tokio::spawn(async move {
let (stream, _) = listener.accept().await.expect("accept should succeed");
let websocket = accept_async(stream)
.await
.expect("websocket upgrade should succeed");
handler(websocket).await;
});
format!("ws://{addr}")
}
async fn expect_remote_initialize(
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
) {
let JSONRPCMessage::Request(request) = read_websocket_message(websocket).await else {
panic!("expected initialize request");
};
assert_eq!(request.method, "initialize");
write_websocket_message(
websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::json!({}),
}),
)
.await;
let JSONRPCMessage::Notification(notification) = read_websocket_message(websocket).await
else {
panic!("expected initialized notification");
};
assert_eq!(notification.method, "initialized");
}
async fn read_websocket_message(
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
) -> JSONRPCMessage {
loop {
let frame = websocket
.next()
.await
.expect("frame should be available")
.expect("frame should decode");
match frame {
Message::Text(text) => {
return serde_json::from_str::<JSONRPCMessage>(&text)
.expect("text frame should be valid JSON-RPC");
}
Message::Binary(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => {
continue;
}
Message::Close(_) => panic!("unexpected close frame"),
}
}
}
async fn write_websocket_message(
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
message: JSONRPCMessage,
) {
websocket
.send(Message::Text(
serde_json::to_string(&message)
.expect("message should serialize")
.into(),
))
.await
.expect("message should send");
}
fn test_remote_connect_args(websocket_url: String) -> RemoteAppServerConnectArgs {
RemoteAppServerConnectArgs {
websocket_url,
client_name: "codex-app-server-client-test".to_string(),
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
channel_capacity: 8,
}
}
#[tokio::test]
async fn typed_request_roundtrip_works() {
let client = start_test_client(SessionSource::Exec).await;
@ -802,6 +1092,354 @@ mod tests {
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_typed_request_roundtrip_works() {
let websocket_url = start_test_remote_server(|mut websocket| async move {
expect_remote_initialize(&mut websocket).await;
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
else {
panic!("expected account/read request");
};
assert_eq!(request.method, "account/read");
write_websocket_message(
&mut websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(GetAccountResponse {
account: None,
requires_openai_auth: false,
})
.expect("response should serialize"),
}),
)
.await;
})
.await;
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
let response: GetAccountResponse = client
.request_typed(ClientRequest::GetAccount {
request_id: RequestId::Integer(1),
params: codex_app_server_protocol::GetAccountParams {
refresh_token: false,
},
})
.await
.expect("typed request should succeed");
assert_eq!(response.account, None);
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_duplicate_request_id_keeps_original_waiter() {
let (first_request_seen_tx, first_request_seen_rx) = tokio::sync::oneshot::channel();
let websocket_url = start_test_remote_server(|mut websocket| async move {
expect_remote_initialize(&mut websocket).await;
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
else {
panic!("expected account/read request");
};
assert_eq!(request.method, "account/read");
first_request_seen_tx
.send(request.id.clone())
.expect("request id should send");
assert!(
timeout(
Duration::from_millis(100),
read_websocket_message(&mut websocket)
)
.await
.is_err(),
"duplicate request should not be forwarded to the server"
);
write_websocket_message(
&mut websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::to_value(GetAccountResponse {
account: None,
requires_openai_auth: false,
})
.expect("response should serialize"),
}),
)
.await;
let _ = websocket.next().await;
})
.await;
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
let first_request_handle = client.request_handle();
let second_request_handle = first_request_handle.clone();
let first_request = tokio::spawn(async move {
first_request_handle
.request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
request_id: RequestId::Integer(1),
params: codex_app_server_protocol::GetAccountParams {
refresh_token: false,
},
})
.await
});
let first_request_id = first_request_seen_rx
.await
.expect("server should observe the first request");
assert_eq!(first_request_id, RequestId::Integer(1));
let second_err = second_request_handle
.request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
request_id: RequestId::Integer(1),
params: codex_app_server_protocol::GetAccountParams {
refresh_token: false,
},
})
.await
.expect_err("duplicate request id should be rejected");
assert_eq!(
second_err.to_string(),
"account/read transport error: duplicate remote app-server request id `1`"
);
let first_response = first_request
.await
.expect("first request task should join")
.expect("first request should succeed");
assert_eq!(
first_response,
GetAccountResponse {
account: None,
requires_openai_auth: false,
}
);
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_notifications_arrive_over_websocket() {
let websocket_url = start_test_remote_server(|mut websocket| async move {
expect_remote_initialize(&mut websocket).await;
write_websocket_message(
&mut websocket,
JSONRPCMessage::Notification(
serde_json::from_value(
serde_json::to_value(ServerNotification::AccountUpdated(
AccountUpdatedNotification {
auth_mode: None,
plan_type: None,
},
))
.expect("notification should serialize"),
)
.expect("notification should convert to JSON-RPC"),
),
)
.await;
})
.await;
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
let event = client.next_event().await.expect("event should arrive");
assert!(matches!(
event,
AppServerEvent::ServerNotification(ServerNotification::AccountUpdated(_))
));
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_server_request_resolution_roundtrip_works() {
let websocket_url = start_test_remote_server(|mut websocket| async move {
expect_remote_initialize(&mut websocket).await;
let request_id = RequestId::String("srv-1".to_string());
let server_request = JSONRPCRequest {
id: request_id.clone(),
method: "item/tool/requestUserInput".to_string(),
params: Some(
serde_json::to_value(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
questions: vec![ToolRequestUserInputQuestion {
id: "question-1".to_string(),
header: "Mode".to_string(),
question: "Pick one".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![]),
}],
})
.expect("params should serialize"),
),
trace: None,
};
write_websocket_message(&mut websocket, JSONRPCMessage::Request(server_request)).await;
let JSONRPCMessage::Response(response) = read_websocket_message(&mut websocket).await
else {
panic!("expected server request response");
};
assert_eq!(response.id, request_id);
})
.await;
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
let AppServerEvent::ServerRequest(request) = client
.next_event()
.await
.expect("request event should arrive")
else {
panic!("expected server request event");
};
client
.resolve_server_request(request.id().clone(), serde_json::json!({}))
.await
.expect("server request should resolve");
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_server_request_received_during_initialize_is_delivered() {
let websocket_url = start_test_remote_server(|mut websocket| async move {
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
else {
panic!("expected initialize request");
};
assert_eq!(request.method, "initialize");
let request_id = RequestId::String("srv-init".to_string());
write_websocket_message(
&mut websocket,
JSONRPCMessage::Request(JSONRPCRequest {
id: request_id.clone(),
method: "item/tool/requestUserInput".to_string(),
params: Some(
serde_json::to_value(ToolRequestUserInputParams {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
questions: vec![ToolRequestUserInputQuestion {
id: "question-1".to_string(),
header: "Mode".to_string(),
question: "Pick one".to_string(),
is_other: false,
is_secret: false,
options: Some(vec![]),
}],
})
.expect("params should serialize"),
),
trace: None,
}),
)
.await;
write_websocket_message(
&mut websocket,
JSONRPCMessage::Response(JSONRPCResponse {
id: request.id,
result: serde_json::json!({}),
}),
)
.await;
let JSONRPCMessage::Notification(notification) =
read_websocket_message(&mut websocket).await
else {
panic!("expected initialized notification");
};
assert_eq!(notification.method, "initialized");
let JSONRPCMessage::Response(response) = read_websocket_message(&mut websocket).await
else {
panic!("expected server request response");
};
assert_eq!(response.id, request_id);
})
.await;
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
let AppServerEvent::ServerRequest(request) = client
.next_event()
.await
.expect("request event should arrive")
else {
panic!("expected server request event");
};
client
.resolve_server_request(request.id().clone(), serde_json::json!({}))
.await
.expect("server request should resolve");
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_unknown_server_request_is_rejected() {
let websocket_url = start_test_remote_server(|mut websocket| async move {
expect_remote_initialize(&mut websocket).await;
let request_id = RequestId::String("srv-unknown".to_string());
write_websocket_message(
&mut websocket,
JSONRPCMessage::Request(JSONRPCRequest {
id: request_id.clone(),
method: "thread/unknown".to_string(),
params: None,
trace: None,
}),
)
.await;
let JSONRPCMessage::Error(response) = read_websocket_message(&mut websocket).await
else {
panic!("expected JSON-RPC error response");
};
assert_eq!(response.id, request_id);
assert_eq!(response.error.code, -32601);
assert_eq!(
response.error.message,
"unsupported remote app-server request `thread/unknown`"
);
})
.await;
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
client.shutdown().await.expect("shutdown should complete");
}
#[tokio::test]
async fn remote_disconnect_surfaces_as_event() {
let websocket_url = start_test_remote_server(|mut websocket| async move {
expect_remote_initialize(&mut websocket).await;
websocket.close(None).await.expect("close should succeed");
})
.await;
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
.await
.expect("remote client should connect");
let event = client
.next_event()
.await
.expect("disconnect event should arrive");
assert!(matches!(event, AppServerEvent::Disconnected { .. }));
}
#[test]
fn typed_request_error_exposes_sources() {
let transport = TypedRequestError::Transport {

View file

@ -0,0 +1,911 @@
/*
This module implements the websocket-backed app-server client transport.
It owns the remote connection lifecycle, including the initialize/initialized
handshake, JSON-RPC request/response routing, server-request resolution, and
notification streaming. The rest of the crate uses the same `AppServerEvent`
surface for both in-process and remote transports, so callers such as
`tui_app_server` can switch between them without changing their higher-level
session logic.
*/
use std::collections::HashMap;
use std::collections::VecDeque;
use std::io::Error as IoError;
use std::io::ErrorKind;
use std::io::Result as IoResult;
use std::time::Duration;
use crate::AppServerEvent;
use crate::RequestResult;
use crate::SHUTDOWN_TIMEOUT;
use crate::TypedRequestError;
use crate::request_method_name;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::ClientNotification;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::Result as JsonRpcResult;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use futures::SinkExt;
use futures::StreamExt;
use serde::de::DeserializeOwned;
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::time::timeout;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use tracing::warn;
use url::Url;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Debug, Clone)]
pub struct RemoteAppServerConnectArgs {
pub websocket_url: String,
pub client_name: String,
pub client_version: String,
pub experimental_api: bool,
pub opt_out_notification_methods: Vec<String>,
pub channel_capacity: usize,
}
impl RemoteAppServerConnectArgs {
fn initialize_params(&self) -> InitializeParams {
let capabilities = InitializeCapabilities {
experimental_api: self.experimental_api,
opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
None
} else {
Some(self.opt_out_notification_methods.clone())
},
};
InitializeParams {
client_info: ClientInfo {
name: self.client_name.clone(),
title: None,
version: self.client_version.clone(),
},
capabilities: Some(capabilities),
}
}
}
enum RemoteClientCommand {
Request {
request: Box<ClientRequest>,
response_tx: oneshot::Sender<IoResult<RequestResult>>,
},
Notify {
notification: ClientNotification,
response_tx: oneshot::Sender<IoResult<()>>,
},
ResolveServerRequest {
request_id: RequestId,
result: JsonRpcResult,
response_tx: oneshot::Sender<IoResult<()>>,
},
RejectServerRequest {
request_id: RequestId,
error: JSONRPCErrorError,
response_tx: oneshot::Sender<IoResult<()>>,
},
Shutdown {
response_tx: oneshot::Sender<IoResult<()>>,
},
}
pub struct RemoteAppServerClient {
command_tx: mpsc::Sender<RemoteClientCommand>,
event_rx: mpsc::Receiver<AppServerEvent>,
pending_events: VecDeque<AppServerEvent>,
worker_handle: tokio::task::JoinHandle<()>,
}
#[derive(Clone)]
pub struct RemoteAppServerRequestHandle {
command_tx: mpsc::Sender<RemoteClientCommand>,
}
impl RemoteAppServerClient {
pub async fn connect(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
let channel_capacity = args.channel_capacity.max(1);
let websocket_url = args.websocket_url.clone();
let url = Url::parse(&websocket_url).map_err(|err| {
IoError::new(
ErrorKind::InvalidInput,
format!("invalid websocket URL `{websocket_url}`: {err}"),
)
})?;
let stream = timeout(CONNECT_TIMEOUT, connect_async(url.as_str()))
.await
.map_err(|_| {
IoError::new(
ErrorKind::TimedOut,
format!("timed out connecting to remote app server at `{websocket_url}`"),
)
})?
.map(|(stream, _response)| stream)
.map_err(|err| {
IoError::other(format!(
"failed to connect to remote app server at `{websocket_url}`: {err}"
))
})?;
let mut stream = stream;
let pending_events = initialize_remote_connection(
&mut stream,
&websocket_url,
args.initialize_params(),
INITIALIZE_TIMEOUT,
)
.await?;
let (command_tx, mut command_rx) = mpsc::channel::<RemoteClientCommand>(channel_capacity);
let (event_tx, event_rx) = mpsc::channel::<AppServerEvent>(channel_capacity);
let worker_handle = tokio::spawn(async move {
let mut pending_requests =
HashMap::<RequestId, oneshot::Sender<IoResult<RequestResult>>>::new();
let mut skipped_events = 0usize;
loop {
tokio::select! {
command = command_rx.recv() => {
let Some(command) = command else {
let _ = stream.close(None).await;
break;
};
match command {
RemoteClientCommand::Request { request, response_tx } => {
let request_id = request_id_from_client_request(&request);
if pending_requests.contains_key(&request_id) {
let _ = response_tx.send(Err(IoError::new(
ErrorKind::InvalidInput,
format!("duplicate remote app-server request id `{request_id}`"),
)));
continue;
}
pending_requests.insert(request_id.clone(), response_tx);
if let Err(err) = write_jsonrpc_message(
&mut stream,
JSONRPCMessage::Request(jsonrpc_request_from_client_request(*request)),
&websocket_url,
)
.await
{
let err_message = err.to_string();
if let Some(response_tx) = pending_requests.remove(&request_id) {
let _ = response_tx.send(Err(err));
}
let _ = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::Disconnected {
message: format!(
"remote app server at `{websocket_url}` write failed: {err_message}"
),
},
&mut stream,
)
.await;
break;
}
}
RemoteClientCommand::Notify { notification, response_tx } => {
let result = write_jsonrpc_message(
&mut stream,
JSONRPCMessage::Notification(
jsonrpc_notification_from_client_notification(notification),
),
&websocket_url,
)
.await;
let _ = response_tx.send(result);
}
RemoteClientCommand::ResolveServerRequest {
request_id,
result,
response_tx,
} => {
let result = write_jsonrpc_message(
&mut stream,
JSONRPCMessage::Response(JSONRPCResponse {
id: request_id,
result,
}),
&websocket_url,
)
.await;
let _ = response_tx.send(result);
}
RemoteClientCommand::RejectServerRequest {
request_id,
error,
response_tx,
} => {
let result = write_jsonrpc_message(
&mut stream,
JSONRPCMessage::Error(JSONRPCError {
error,
id: request_id,
}),
&websocket_url,
)
.await;
let _ = response_tx.send(result);
}
RemoteClientCommand::Shutdown { response_tx } => {
let close_result = stream.close(None).await.map_err(|err| {
IoError::other(format!(
"failed to close websocket app server `{websocket_url}`: {err}"
))
});
let _ = response_tx.send(close_result);
break;
}
}
}
message = stream.next() => {
match message {
Some(Ok(Message::Text(text))) => {
match serde_json::from_str::<JSONRPCMessage>(&text) {
Ok(JSONRPCMessage::Response(response)) => {
if let Some(response_tx) = pending_requests.remove(&response.id) {
let _ = response_tx.send(Ok(Ok(response.result)));
}
}
Ok(JSONRPCMessage::Error(error)) => {
if let Some(response_tx) = pending_requests.remove(&error.id) {
let _ = response_tx.send(Ok(Err(error.error)));
}
}
Ok(JSONRPCMessage::Notification(notification)) => {
let event = app_server_event_from_notification(notification);
if let Err(err) = deliver_event(
&event_tx,
&mut skipped_events,
event,
&mut stream,
)
.await
{
warn!(%err, "failed to deliver remote app-server event");
break;
}
}
Ok(JSONRPCMessage::Request(request)) => {
let request_id = request.id.clone();
let method = request.method.clone();
match ServerRequest::try_from(request) {
Ok(request) => {
if let Err(err) = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::ServerRequest(request),
&mut stream,
)
.await
{
warn!(%err, "failed to deliver remote app-server server request");
break;
}
}
Err(err) => {
warn!(%err, method, "rejecting unknown remote app-server request");
if let Err(reject_err) = write_jsonrpc_message(
&mut stream,
JSONRPCMessage::Error(JSONRPCError {
error: JSONRPCErrorError {
code: -32601,
message: format!(
"unsupported remote app-server request `{method}`"
),
data: None,
},
id: request_id,
}),
&websocket_url,
)
.await
{
let err_message = reject_err.to_string();
let _ = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::Disconnected {
message: format!(
"remote app server at `{websocket_url}` write failed: {err_message}"
),
},
&mut stream,
)
.await;
break;
}
}
}
}
Err(err) => {
let _ = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::Disconnected {
message: format!(
"remote app server at `{websocket_url}` sent invalid JSON-RPC: {err}"
),
},
&mut stream,
)
.await;
break;
}
}
}
Some(Ok(Message::Close(frame))) => {
let reason = frame
.as_ref()
.map(|frame| frame.reason.to_string())
.filter(|reason| !reason.is_empty())
.unwrap_or_else(|| "connection closed".to_string());
let _ = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::Disconnected {
message: format!(
"remote app server at `{websocket_url}` disconnected: {reason}"
),
},
&mut stream,
)
.await;
break;
}
Some(Ok(Message::Binary(_)))
| Some(Ok(Message::Ping(_)))
| Some(Ok(Message::Pong(_)))
| Some(Ok(Message::Frame(_))) => {}
Some(Err(err)) => {
let _ = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::Disconnected {
message: format!(
"remote app server at `{websocket_url}` transport failed: {err}"
),
},
&mut stream,
)
.await;
break;
}
None => {
let _ = deliver_event(
&event_tx,
&mut skipped_events,
AppServerEvent::Disconnected {
message: format!(
"remote app server at `{websocket_url}` closed the connection"
),
},
&mut stream,
)
.await;
break;
}
}
}
}
}
let err = IoError::new(
ErrorKind::BrokenPipe,
"remote app-server worker channel is closed",
);
for (_, response_tx) in pending_requests {
let _ = response_tx.send(Err(IoError::new(err.kind(), err.to_string())));
}
});
Ok(Self {
command_tx,
event_rx,
pending_events: pending_events.into(),
worker_handle,
})
}
pub fn request_handle(&self) -> RemoteAppServerRequestHandle {
RemoteAppServerRequestHandle {
command_tx: self.command_tx.clone(),
}
}
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
let (response_tx, response_rx) = oneshot::channel();
self.command_tx
.send(RemoteClientCommand::Request {
request: Box::new(request),
response_tx,
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server worker channel is closed",
)
})?;
response_rx.await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server request channel is closed",
)
})?
}
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
where
T: DeserializeOwned,
{
let method = request_method_name(&request);
let response =
self.request(request)
.await
.map_err(|source| TypedRequestError::Transport {
method: method.clone(),
source,
})?;
let result = response.map_err(|source| TypedRequestError::Server {
method: method.clone(),
source,
})?;
serde_json::from_value(result)
.map_err(|source| TypedRequestError::Deserialize { method, source })
}
pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
let (response_tx, response_rx) = oneshot::channel();
self.command_tx
.send(RemoteClientCommand::Notify {
notification,
response_tx,
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server worker channel is closed",
)
})?;
response_rx.await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server notify channel is closed",
)
})?
}
pub async fn resolve_server_request(
&self,
request_id: RequestId,
result: JsonRpcResult,
) -> IoResult<()> {
let (response_tx, response_rx) = oneshot::channel();
self.command_tx
.send(RemoteClientCommand::ResolveServerRequest {
request_id,
result,
response_tx,
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server worker channel is closed",
)
})?;
response_rx.await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server resolve channel is closed",
)
})?
}
pub async fn reject_server_request(
&self,
request_id: RequestId,
error: JSONRPCErrorError,
) -> IoResult<()> {
let (response_tx, response_rx) = oneshot::channel();
self.command_tx
.send(RemoteClientCommand::RejectServerRequest {
request_id,
error,
response_tx,
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server worker channel is closed",
)
})?;
response_rx.await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server reject channel is closed",
)
})?
}
pub async fn next_event(&mut self) -> Option<AppServerEvent> {
if let Some(event) = self.pending_events.pop_front() {
return Some(event);
}
self.event_rx.recv().await
}
pub async fn shutdown(self) -> IoResult<()> {
let Self {
command_tx,
event_rx,
pending_events: _pending_events,
worker_handle,
} = self;
let mut worker_handle = worker_handle;
drop(event_rx);
let (response_tx, response_rx) = oneshot::channel();
if command_tx
.send(RemoteClientCommand::Shutdown { response_tx })
.await
.is_ok()
&& let Ok(command_result) = timeout(SHUTDOWN_TIMEOUT, response_rx).await
{
command_result.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server shutdown channel is closed",
)
})??;
}
if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await {
worker_handle.abort();
let _ = worker_handle.await;
}
Ok(())
}
}
impl RemoteAppServerRequestHandle {
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
let (response_tx, response_rx) = oneshot::channel();
self.command_tx
.send(RemoteClientCommand::Request {
request: Box::new(request),
response_tx,
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server worker channel is closed",
)
})?;
response_rx.await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server request channel is closed",
)
})?
}
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
where
T: DeserializeOwned,
{
let method = request_method_name(&request);
let response =
self.request(request)
.await
.map_err(|source| TypedRequestError::Transport {
method: method.clone(),
source,
})?;
let result = response.map_err(|source| TypedRequestError::Server {
method: method.clone(),
source,
})?;
serde_json::from_value(result)
.map_err(|source| TypedRequestError::Deserialize { method, source })
}
}
async fn initialize_remote_connection(
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
websocket_url: &str,
params: InitializeParams,
initialize_timeout: Duration,
) -> IoResult<Vec<AppServerEvent>> {
let initialize_request_id = RequestId::String("initialize".to_string());
let mut pending_events = Vec::new();
write_jsonrpc_message(
stream,
JSONRPCMessage::Request(jsonrpc_request_from_client_request(
ClientRequest::Initialize {
request_id: initialize_request_id.clone(),
params,
},
)),
websocket_url,
)
.await?;
timeout(initialize_timeout, async {
loop {
match stream.next().await {
Some(Ok(Message::Text(text))) => {
let message = serde_json::from_str::<JSONRPCMessage>(&text).map_err(|err| {
IoError::other(format!(
"remote app server at `{websocket_url}` sent invalid initialize response: {err}"
))
})?;
match message {
JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
break Ok(());
}
JSONRPCMessage::Error(error) if error.id == initialize_request_id => {
break Err(IoError::other(format!(
"remote app server at `{websocket_url}` rejected initialize: {}",
error.error.message
)));
}
JSONRPCMessage::Notification(notification) => {
pending_events.push(app_server_event_from_notification(notification));
}
JSONRPCMessage::Request(request) => {
let request_id = request.id.clone();
let method = request.method.clone();
match ServerRequest::try_from(request) {
Ok(request) => {
pending_events.push(AppServerEvent::ServerRequest(request));
}
Err(err) => {
warn!(%err, method, "rejecting unknown remote app-server request during initialize");
write_jsonrpc_message(
stream,
JSONRPCMessage::Error(JSONRPCError {
error: JSONRPCErrorError {
code: -32601,
message: format!(
"unsupported remote app-server request `{method}`"
),
data: None,
},
id: request_id,
}),
websocket_url,
)
.await?;
}
}
}
JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => {}
}
}
Some(Ok(Message::Binary(_)))
| Some(Ok(Message::Ping(_)))
| Some(Ok(Message::Pong(_)))
| Some(Ok(Message::Frame(_))) => {}
Some(Ok(Message::Close(frame))) => {
let reason = frame
.as_ref()
.map(|frame| frame.reason.to_string())
.filter(|reason| !reason.is_empty())
.unwrap_or_else(|| "connection closed during initialize".to_string());
break Err(IoError::new(
ErrorKind::ConnectionAborted,
format!(
"remote app server at `{websocket_url}` closed during initialize: {reason}"
),
));
}
Some(Err(err)) => {
break Err(IoError::other(format!(
"remote app server at `{websocket_url}` transport failed during initialize: {err}"
)));
}
None => {
break Err(IoError::new(
ErrorKind::UnexpectedEof,
format!("remote app server at `{websocket_url}` closed during initialize"),
));
}
}
}
})
.await
.map_err(|_| {
IoError::new(
ErrorKind::TimedOut,
format!("timed out waiting for initialize response from `{websocket_url}`"),
)
})??;
write_jsonrpc_message(
stream,
JSONRPCMessage::Notification(jsonrpc_notification_from_client_notification(
ClientNotification::Initialized,
)),
websocket_url,
)
.await?;
Ok(pending_events)
}
fn app_server_event_from_notification(notification: JSONRPCNotification) -> AppServerEvent {
match ServerNotification::try_from(notification.clone()) {
Ok(notification) => AppServerEvent::ServerNotification(notification),
Err(_) => AppServerEvent::LegacyNotification(notification),
}
}
async fn deliver_event(
event_tx: &mpsc::Sender<AppServerEvent>,
skipped_events: &mut usize,
event: AppServerEvent,
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
) -> IoResult<()> {
if *skipped_events > 0 {
if event_requires_delivery(&event) {
if event_tx
.send(AppServerEvent::Lagged {
skipped: *skipped_events,
})
.await
.is_err()
{
return Err(IoError::new(
ErrorKind::BrokenPipe,
"remote app-server event consumer channel is closed",
));
}
*skipped_events = 0;
} else {
match event_tx.try_send(AppServerEvent::Lagged {
skipped: *skipped_events,
}) {
Ok(()) => *skipped_events = 0,
Err(mpsc::error::TrySendError::Full(_)) => {
*skipped_events = (*skipped_events).saturating_add(1);
reject_if_server_request_dropped(stream, &event).await?;
return Ok(());
}
Err(mpsc::error::TrySendError::Closed(_)) => {
return Err(IoError::new(
ErrorKind::BrokenPipe,
"remote app-server event consumer channel is closed",
));
}
}
}
}
if event_requires_delivery(&event) {
event_tx.send(event).await.map_err(|_| {
IoError::new(
ErrorKind::BrokenPipe,
"remote app-server event consumer channel is closed",
)
})?;
return Ok(());
}
match event_tx.try_send(event) {
Ok(()) => Ok(()),
Err(mpsc::error::TrySendError::Full(event)) => {
*skipped_events = (*skipped_events).saturating_add(1);
reject_if_server_request_dropped(stream, &event).await
}
Err(mpsc::error::TrySendError::Closed(_)) => Err(IoError::new(
ErrorKind::BrokenPipe,
"remote app-server event consumer channel is closed",
)),
}
}
async fn reject_if_server_request_dropped(
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
event: &AppServerEvent,
) -> IoResult<()> {
let AppServerEvent::ServerRequest(request) = event else {
return Ok(());
};
write_jsonrpc_message(
stream,
JSONRPCMessage::Error(JSONRPCError {
error: JSONRPCErrorError {
code: -32001,
message: "remote app-server event queue is full".to_string(),
data: None,
},
id: request.id().clone(),
}),
"<remote-app-server>",
)
.await
}
fn event_requires_delivery(event: &AppServerEvent) -> bool {
match event {
AppServerEvent::ServerNotification(ServerNotification::TurnCompleted(_)) => true,
AppServerEvent::LegacyNotification(notification) => matches!(
notification
.method
.strip_prefix("codex/event/")
.unwrap_or(&notification.method),
"task_complete" | "turn_aborted" | "shutdown_complete"
),
AppServerEvent::Disconnected { .. } => true,
AppServerEvent::Lagged { .. }
| AppServerEvent::ServerNotification(_)
| AppServerEvent::ServerRequest(_) => false,
}
}
fn request_id_from_client_request(request: &ClientRequest) -> RequestId {
jsonrpc_request_from_client_request(request.clone()).id
}
fn jsonrpc_request_from_client_request(request: ClientRequest) -> JSONRPCRequest {
let value = match serde_json::to_value(request) {
Ok(value) => value,
Err(err) => panic!("client request should serialize: {err}"),
};
match serde_json::from_value(value) {
Ok(request) => request,
Err(err) => panic!("client request should encode as JSON-RPC request: {err}"),
}
}
fn jsonrpc_notification_from_client_notification(
notification: ClientNotification,
) -> JSONRPCNotification {
let value = match serde_json::to_value(notification) {
Ok(value) => value,
Err(err) => panic!("client notification should serialize: {err}"),
};
match serde_json::from_value(value) {
Ok(notification) => notification,
Err(err) => panic!("client notification should encode as JSON-RPC notification: {err}"),
}
}
async fn write_jsonrpc_message(
stream: &mut WebSocketStream<MaybeTlsStream<TcpStream>>,
message: JSONRPCMessage,
websocket_url: &str,
) -> IoResult<()> {
let payload = serde_json::to_string(&message).map_err(IoError::other)?;
stream
.send(Message::Text(payload.into()))
.await
.map_err(|err| {
IoError::other(format!(
"failed to write websocket message to `{websocket_url}`: {err}"
))
})
}

View file

@ -38,6 +38,7 @@ codex-rmcp-client = { workspace = true }
codex-state = { workspace = true }
codex-stdio-to-uds = { workspace = true }
codex-tui = { workspace = true }
codex-tui-app-server = { workspace = true }
libc = { workspace = true }
owo-colors = { workspace = true }
regex-lite = { workspace = true }

View file

@ -74,6 +74,9 @@ struct MultitoolCli {
#[clap(flatten)]
pub feature_toggles: FeatureToggles,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
interactive: TuiCli,
@ -204,6 +207,9 @@ struct ResumeCommand {
#[arg(long = "all", default_value_t = false)]
all: bool,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
config_overrides: TuiCli,
}
@ -223,6 +229,9 @@ struct ForkCommand {
#[arg(long = "all", default_value_t = false)]
all: bool,
#[clap(flatten)]
remote: InteractiveRemoteOptions,
#[clap(flatten)]
config_overrides: TuiCli,
}
@ -494,6 +503,15 @@ struct FeatureToggles {
disable: Vec<String>,
}
#[derive(Debug, Default, Parser, Clone)]
struct InteractiveRemoteOptions {
/// Connect the app-server-backed TUI to a remote app server websocket endpoint.
///
/// Accepted forms: `ws://host:port` or `wss://host:port`.
#[arg(long = "remote", value_name = "ADDR")]
remote: Option<String>,
}
impl FeatureToggles {
fn to_overrides(&self) -> anyhow::Result<Vec<String>> {
let mut v = Vec::new();
@ -561,6 +579,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
let MultitoolCli {
config_overrides: mut root_config_overrides,
feature_toggles,
remote,
mut interactive,
subcommand,
} = MultitoolCli::parse();
@ -568,6 +587,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
// Fold --enable/--disable into config overrides so they flow to all subcommands.
let toggle_overrides = feature_toggles.to_overrides()?;
root_config_overrides.raw_overrides.extend(toggle_overrides);
let root_remote = remote.remote;
match subcommand {
None => {
@ -575,10 +595,12 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
&mut interactive.config_overrides,
root_config_overrides.clone(),
);
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
let exit_info =
run_interactive_tui(interactive, root_remote.clone(), arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Exec(mut exec_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "exec")?;
prepend_config_flags(
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
@ -586,6 +608,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::Review(review_args)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "review")?;
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
exec_cli.command = Some(ExecCommand::Review(review_args));
prepend_config_flags(
@ -595,15 +618,18 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::McpServer) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "mcp-server")?;
codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?;
}
Some(Subcommand::Mcp(mut mcp_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "mcp")?;
// Propagate any root-level config overrides (e.g. `-c key=value`).
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
mcp_cli.run().await?;
}
Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand {
None => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app-server")?;
let transport = app_server_cli.listen;
codex_app_server::run_main_with_transport(
arg0_paths.clone(),
@ -615,6 +641,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
.await?;
}
Some(AppServerSubcommand::GenerateTs(gen_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
"app-server generate-ts",
)?;
let options = codex_app_server_protocol::GenerateTsOptions {
experimental_api: gen_cli.experimental,
..Default::default()
@ -626,6 +656,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
)?;
}
Some(AppServerSubcommand::GenerateJsonSchema(gen_cli)) => {
reject_remote_mode_for_subcommand(
root_remote.as_deref(),
"app-server generate-json-schema",
)?;
codex_app_server_protocol::generate_json_with_experimental(
&gen_cli.out_dir,
gen_cli.experimental,
@ -634,12 +668,14 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
},
#[cfg(target_os = "macos")]
Some(Subcommand::App(app_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app")?;
app_cmd::run_app(app_cli).await?;
}
Some(Subcommand::Resume(ResumeCommand {
session_id,
last,
all,
remote,
config_overrides,
})) => {
interactive = finalize_resume_interactive(
@ -650,13 +686,19 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
all,
config_overrides,
);
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
let exit_info = run_interactive_tui(
interactive,
remote.remote.or(root_remote.clone()),
arg0_paths.clone(),
)
.await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Fork(ForkCommand {
session_id,
last,
all,
remote,
config_overrides,
})) => {
interactive = finalize_fork_interactive(
@ -667,10 +709,16 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
all,
config_overrides,
);
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
let exit_info = run_interactive_tui(
interactive,
remote.remote.or(root_remote.clone()),
arg0_paths.clone(),
)
.await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Login(mut login_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "login")?;
prepend_config_flags(
&mut login_cli.config_overrides,
root_config_overrides.clone(),
@ -702,6 +750,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
}
}
Some(Subcommand::Logout(mut logout_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "logout")?;
prepend_config_flags(
&mut logout_cli.config_overrides,
root_config_overrides.clone(),
@ -709,9 +758,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
run_logout(logout_cli.config_overrides).await;
}
Some(Subcommand::Completion(completion_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "completion")?;
print_completion(completion_cli);
}
Some(Subcommand::Cloud(mut cloud_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "cloud")?;
prepend_config_flags(
&mut cloud_cli.config_overrides,
root_config_overrides.clone(),
@ -721,6 +772,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
}
Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd {
SandboxCommand::Macos(mut seatbelt_cli) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox macos")?;
prepend_config_flags(
&mut seatbelt_cli.config_overrides,
root_config_overrides.clone(),
@ -732,6 +784,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
.await?;
}
SandboxCommand::Linux(mut landlock_cli) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox linux")?;
prepend_config_flags(
&mut landlock_cli.config_overrides,
root_config_overrides.clone(),
@ -743,6 +796,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
.await?;
}
SandboxCommand::Windows(mut windows_cli) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox windows")?;
prepend_config_flags(
&mut windows_cli.config_overrides,
root_config_overrides.clone(),
@ -756,16 +810,22 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
},
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
DebugSubcommand::AppServer(cmd) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "debug app-server")?;
run_debug_app_server_command(cmd).await?;
}
DebugSubcommand::ClearMemories => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "debug clear-memories")?;
run_debug_clear_memories_command(&root_config_overrides, &interactive).await?;
}
},
Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub {
ExecpolicySubcommand::Check(cmd) => run_execpolicycheck(cmd)?,
ExecpolicySubcommand::Check(cmd) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "execpolicy check")?;
run_execpolicycheck(cmd)?
}
},
Some(Subcommand::Apply(mut apply_cli)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "apply")?;
prepend_config_flags(
&mut apply_cli.config_overrides,
root_config_overrides.clone(),
@ -773,16 +833,19 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
run_apply_command(apply_cli, None).await?;
}
Some(Subcommand::ResponsesApiProxy(args)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "responses-api-proxy")?;
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
.await??;
}
Some(Subcommand::StdioToUds(cmd)) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "stdio-to-uds")?;
let socket_path = cmd.socket_path;
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
.await??;
}
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
FeaturesSubcommand::List => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features list")?;
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
let mut cli_kv_overrides = root_config_overrides
.parse_overrides()
@ -825,9 +888,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
}
}
FeaturesSubcommand::Enable(FeatureSetArgs { feature }) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features enable")?;
enable_feature_in_config(&interactive, &feature).await?;
}
FeaturesSubcommand::Disable(FeatureSetArgs { feature }) => {
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features disable")?;
disable_feature_in_config(&interactive, &feature).await?;
}
},
@ -949,8 +1014,18 @@ fn prepend_config_flags(
.splice(0..0, cli_config_overrides.raw_overrides);
}
fn reject_remote_mode_for_subcommand(remote: Option<&str>, subcommand: &str) -> anyhow::Result<()> {
if let Some(remote) = remote {
anyhow::bail!(
"`--remote {remote}` is only supported for interactive TUI commands, not `codex {subcommand}`"
);
}
Ok(())
}
async fn run_interactive_tui(
mut interactive: TuiCli,
remote: Option<String>,
arg0_paths: Arg0DispatchPaths,
) -> std::io::Result<AppExitInfo> {
if let Some(prompt) = interactive.prompt.take() {
@ -976,12 +1051,93 @@ async fn run_interactive_tui(
}
}
codex_tui::run_main(
interactive,
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
)
.await
let use_app_server_tui = codex_tui::should_use_app_server_tui(&interactive).await?;
let normalized_remote = remote
.as_deref()
.map(codex_tui_app_server::normalize_remote_addr)
.transpose()
.map_err(std::io::Error::other)?;
if normalized_remote.is_some() && !use_app_server_tui {
return Ok(AppExitInfo::fatal(
"`--remote` requires the `tui_app_server` feature flag to be enabled.",
));
}
if use_app_server_tui {
codex_tui_app_server::run_main(
into_app_server_tui_cli(interactive),
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
normalized_remote,
)
.await
.map(into_legacy_app_exit_info)
} else {
codex_tui::run_main(
interactive,
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
)
.await
}
}
fn into_app_server_tui_cli(cli: TuiCli) -> codex_tui_app_server::Cli {
codex_tui_app_server::Cli {
prompt: cli.prompt,
images: cli.images,
resume_picker: cli.resume_picker,
resume_last: cli.resume_last,
resume_session_id: cli.resume_session_id,
resume_show_all: cli.resume_show_all,
fork_picker: cli.fork_picker,
fork_last: cli.fork_last,
fork_session_id: cli.fork_session_id,
fork_show_all: cli.fork_show_all,
model: cli.model,
oss: cli.oss,
oss_provider: cli.oss_provider,
config_profile: cli.config_profile,
sandbox_mode: cli.sandbox_mode,
approval_policy: cli.approval_policy,
full_auto: cli.full_auto,
dangerously_bypass_approvals_and_sandbox: cli.dangerously_bypass_approvals_and_sandbox,
cwd: cli.cwd,
web_search: cli.web_search,
add_dir: cli.add_dir,
no_alt_screen: cli.no_alt_screen,
config_overrides: cli.config_overrides,
}
}
fn into_legacy_update_action(
action: codex_tui_app_server::update_action::UpdateAction,
) -> UpdateAction {
match action {
codex_tui_app_server::update_action::UpdateAction::NpmGlobalLatest => {
UpdateAction::NpmGlobalLatest
}
codex_tui_app_server::update_action::UpdateAction::BunGlobalLatest => {
UpdateAction::BunGlobalLatest
}
codex_tui_app_server::update_action::UpdateAction::BrewUpgrade => UpdateAction::BrewUpgrade,
}
}
fn into_legacy_exit_reason(reason: codex_tui_app_server::ExitReason) -> ExitReason {
match reason {
codex_tui_app_server::ExitReason::UserRequested => ExitReason::UserRequested,
codex_tui_app_server::ExitReason::Fatal(message) => ExitReason::Fatal(message),
}
}
fn into_legacy_app_exit_info(exit_info: codex_tui_app_server::AppExitInfo) -> AppExitInfo {
AppExitInfo {
token_usage: exit_info.token_usage,
thread_id: exit_info.thread_id,
thread_name: exit_info.thread_name,
update_action: exit_info.update_action.map(into_legacy_update_action),
exit_reason: into_legacy_exit_reason(exit_info.exit_reason),
}
}
fn confirm(prompt: &str) -> std::io::Result<bool> {
@ -1114,12 +1270,14 @@ mod tests {
config_overrides: root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
let Subcommand::Resume(ResumeCommand {
session_id,
last,
all,
remote: _,
config_overrides: resume_cli,
}) = subcommand.expect("resume present")
else {
@ -1143,12 +1301,14 @@ mod tests {
config_overrides: root_overrides,
subcommand,
feature_toggles: _,
remote: _,
} = cli;
let Subcommand::Fork(ForkCommand {
session_id,
last,
all,
remote: _,
config_overrides: fork_cli,
}) = subcommand.expect("fork present")
else {
@ -1449,6 +1609,36 @@ mod tests {
assert!(app_server.analytics_default_enabled);
}
#[test]
fn remote_flag_parses_for_interactive_root() {
let cli = MultitoolCli::try_parse_from(["codex", "--remote", "ws://127.0.0.1:4500"])
.expect("parse");
assert_eq!(cli.remote.remote.as_deref(), Some("ws://127.0.0.1:4500"));
}
#[test]
fn remote_flag_parses_for_resume_subcommand() {
let cli =
MultitoolCli::try_parse_from(["codex", "resume", "--remote", "ws://127.0.0.1:4500"])
.expect("parse");
let Subcommand::Resume(ResumeCommand { remote, .. }) =
cli.subcommand.expect("resume present")
else {
panic!("expected resume subcommand");
};
assert_eq!(remote.remote.as_deref(), Some("ws://127.0.0.1:4500"));
}
#[test]
fn reject_remote_mode_for_non_interactive_subcommands() {
let err = reject_remote_mode_for_subcommand(Some("127.0.0.1:4500"), "exec")
.expect_err("non-interactive subcommands should reject --remote");
assert!(
err.to_string()
.contains("only supported for interactive TUI commands")
);
}
#[test]
fn app_server_listen_websocket_url_parses() {
let app_server = app_server_from_args(

View file

@ -16,6 +16,7 @@ use chrono::Duration as ChronoDuration;
use chrono::Utc;
use codex_backend_client::Client as BackendClient;
use codex_core::AuthManager;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::auth::CodexAuth;
use codex_core::auth::RefreshTokenError;
use codex_core::config_loader::CloudRequirementsLoadError;
@ -715,6 +716,20 @@ pub fn cloud_requirements_loader(
})
}
pub fn cloud_requirements_loader_for_storage(
codex_home: PathBuf,
enable_codex_api_key_env: bool,
credentials_store_mode: AuthCredentialsStoreMode,
chatgpt_base_url: String,
) -> CloudRequirementsLoader {
let auth_manager = AuthManager::shared(
codex_home.clone(),
enable_codex_api_key_env,
credentials_store_mode,
);
cloud_requirements_loader(auth_manager, chatgpt_base_url, codex_home)
}
fn parse_cloud_requirements(
contents: &str,
) -> Result<Option<ConfigRequirementsToml>, toml::de::Error> {

View file

@ -1,5 +1,13 @@
load("//:defs.bzl", "codex_rust_crate")
exports_files(
[
"templates/collaboration_mode/default.md",
"templates/collaboration_mode/plan.md",
],
visibility = ["//visibility:public"],
)
filegroup(
name = "model_availability_nux_fixtures",
srcs = [

View file

@ -488,6 +488,9 @@
"tool_suggest": {
"type": "boolean"
},
"tui_app_server": {
"type": "boolean"
},
"undo": {
"type": "boolean"
},
@ -2037,6 +2040,9 @@
"tool_suggest": {
"type": "boolean"
},
"tui_app_server": {
"type": "boolean"
},
"undo": {
"type": "boolean"
},

View file

@ -180,6 +180,8 @@ pub enum Feature {
VoiceTranscription,
/// Enable experimental realtime voice conversation mode in the TUI.
RealtimeConversation,
/// Route interactive startup to the app-server-backed TUI implementation.
TuiAppServer,
/// Prevent idle system sleep while a turn is actively running.
PreventIdleSleep,
/// Use the Responses API WebSocket transport for OpenAI by default.
@ -827,6 +829,16 @@ pub const FEATURES: &[FeatureSpec] = &[
stage: Stage::UnderDevelopment,
default_enabled: false,
},
FeatureSpec {
id: Feature::TuiAppServer,
key: "tui_app_server",
stage: Stage::Experimental {
name: "App-server TUI",
menu_description: "Use the app-server-backed TUI implementation.",
announcement: "",
},
default_enabled: false,
},
FeatureSpec {
id: Feature::PreventIdleSleep,
key: "prevent_idle_sleep",

View file

@ -44,6 +44,7 @@ codex-otel = { workspace = true }
codex-protocol = { workspace = true }
codex-shell-command = { workspace = true }
codex-state = { workspace = true }
codex-tui-app-server = { workspace = true }
codex-utils-approval-presets = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cli = { workspace = true }

View file

@ -0,0 +1,45 @@
use std::future::Future;
use crate::Cli;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::features::Feature;
pub(crate) fn app_server_tui_config_inputs(
cli: &Cli,
) -> std::io::Result<(Vec<(String, toml::Value)>, ConfigOverrides)> {
let mut raw_overrides = cli.config_overrides.raw_overrides.clone();
if cli.web_search {
raw_overrides.push("web_search=\"live\"".to_string());
}
let cli_kv_overrides = codex_utils_cli::CliConfigOverrides { raw_overrides }
.parse_overrides()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidInput, err))?;
let config_overrides = ConfigOverrides {
cwd: cli.cwd.clone(),
config_profile: cli.config_profile.clone(),
..Default::default()
};
Ok((cli_kv_overrides, config_overrides))
}
pub(crate) async fn should_use_app_server_tui_with<F, Fut>(
cli: &Cli,
load_config: F,
) -> std::io::Result<bool>
where
F: FnOnce(Vec<(String, toml::Value)>, ConfigOverrides) -> Fut,
Fut: Future<Output = std::io::Result<Config>>,
{
let (cli_kv_overrides, config_overrides) = app_server_tui_config_inputs(cli)?;
let config = load_config(cli_kv_overrides, config_overrides).await?;
Ok(config.features.enabled(Feature::TuiAppServer))
}
pub async fn should_use_app_server_tui(cli: &Cli) -> std::io::Result<bool> {
should_use_app_server_tui_with(cli, Config::load_with_cli_overrides_and_harness_overrides).await
}

View file

@ -70,6 +70,7 @@ mod app;
mod app_backtrack;
mod app_event;
mod app_event_sender;
mod app_server_tui_dispatch;
mod ascii_animation;
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
mod audio_device;
@ -229,6 +230,7 @@ pub mod test_backend;
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
use crate::onboarding::onboarding_screen::run_onboarding_app;
use crate::tui::Tui;
pub use app_server_tui_dispatch::should_use_app_server_tui;
pub use cli::Cli;
use codex_arg0::Arg0DispatchPaths;
pub use markdown_render::render_markdown_text;
@ -1288,6 +1290,7 @@ mod tests {
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::TurnContextItem;
use pretty_assertions::assert_eq;
use serial_test::serial;
use tempfile::TempDir;

View file

@ -1,8 +1,11 @@
use clap::Parser;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_tui::AppExitInfo;
use codex_tui::Cli;
use codex_tui::ExitReason;
use codex_tui::run_main;
use codex_tui::update_action::UpdateAction;
use codex_utils_cli::CliConfigOverrides;
#[derive(Parser, Debug)]
@ -14,6 +17,65 @@ struct TopCli {
inner: Cli,
}
fn into_app_server_cli(cli: Cli) -> codex_tui_app_server::Cli {
codex_tui_app_server::Cli {
prompt: cli.prompt,
images: cli.images,
resume_picker: cli.resume_picker,
resume_last: cli.resume_last,
resume_session_id: cli.resume_session_id,
resume_show_all: cli.resume_show_all,
fork_picker: cli.fork_picker,
fork_last: cli.fork_last,
fork_session_id: cli.fork_session_id,
fork_show_all: cli.fork_show_all,
model: cli.model,
oss: cli.oss,
oss_provider: cli.oss_provider,
config_profile: cli.config_profile,
sandbox_mode: cli.sandbox_mode,
approval_policy: cli.approval_policy,
full_auto: cli.full_auto,
dangerously_bypass_approvals_and_sandbox: cli.dangerously_bypass_approvals_and_sandbox,
cwd: cli.cwd,
web_search: cli.web_search,
add_dir: cli.add_dir,
no_alt_screen: cli.no_alt_screen,
config_overrides: cli.config_overrides,
}
}
fn into_legacy_update_action(
action: codex_tui_app_server::update_action::UpdateAction,
) -> UpdateAction {
match action {
codex_tui_app_server::update_action::UpdateAction::NpmGlobalLatest => {
UpdateAction::NpmGlobalLatest
}
codex_tui_app_server::update_action::UpdateAction::BunGlobalLatest => {
UpdateAction::BunGlobalLatest
}
codex_tui_app_server::update_action::UpdateAction::BrewUpgrade => UpdateAction::BrewUpgrade,
}
}
fn into_legacy_exit_reason(reason: codex_tui_app_server::ExitReason) -> ExitReason {
match reason {
codex_tui_app_server::ExitReason::UserRequested => ExitReason::UserRequested,
codex_tui_app_server::ExitReason::Fatal(message) => ExitReason::Fatal(message),
}
}
fn into_legacy_exit_info(exit_info: codex_tui_app_server::AppExitInfo) -> AppExitInfo {
AppExitInfo {
token_usage: exit_info.token_usage,
thread_id: exit_info.thread_id,
thread_name: exit_info.thread_name,
update_action: exit_info.update_action.map(into_legacy_update_action),
exit_reason: into_legacy_exit_reason(exit_info.exit_reason),
}
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
let top_cli = TopCli::parse();
@ -22,12 +84,25 @@ fn main() -> anyhow::Result<()> {
.config_overrides
.raw_overrides
.splice(0..0, top_cli.config_overrides.raw_overrides);
let exit_info = run_main(
inner,
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
)
.await?;
let use_app_server_tui = codex_tui::should_use_app_server_tui(&inner).await?;
let exit_info = if use_app_server_tui {
into_legacy_exit_info(
codex_tui_app_server::run_main(
into_app_server_cli(inner),
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
None,
)
.await?,
)
} else {
run_main(
inner,
arg0_paths,
codex_core::config_loader::LoaderOverrides::default(),
)
.await?
};
let token_usage = exit_info.token_usage;
if !token_usage.is_zero() {
println!(

View file

@ -0,0 +1,23 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "tui_app_server",
crate_name = "codex_tui_app_server",
compile_data = glob(
include = ["**"],
exclude = [
"**/* *",
"BUILD.bazel",
"Cargo.toml",
],
allow_empty = True,
) + [
"//codex-rs/core:templates/collaboration_mode/default.md",
"//codex-rs/core:templates/collaboration_mode/plan.md",
],
test_data_extra = glob(["src/**/snapshots/**"]) + ["//codex-rs/core:model_availability_nux_fixtures"],
integration_compile_data_extra = ["src/test_backend.rs"],
extra_binaries = [
"//codex-rs/cli:codex",
],
)

View file

@ -0,0 +1,149 @@
[package]
name = "codex-tui-app-server"
version.workspace = true
edition.workspace = true
license.workspace = true
autobins = false
[[bin]]
name = "codex-tui-app-server"
path = "src/main.rs"
[[bin]]
name = "md-events-app-server"
path = "src/bin/md-events.rs"
[lib]
name = "codex_tui_app_server"
path = "src/lib.rs"
[features]
default = ["voice-input"]
# Enable vt100-based tests (emulator) when running with `--features vt100-tests`.
vt100-tests = []
# Gate verbose debug logging inside the TUI implementation.
debug-logs = []
voice-input = ["dep:cpal", "dep:hound"]
[lints]
workspace = true
[dependencies]
anyhow = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true, features = ["derive"] }
codex-ansi-escape = { workspace = true }
codex-app-server-client = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-arg0 = { workspace = true }
codex-chatgpt = { workspace = true }
codex-client = { workspace = true }
codex-cloud-requirements = { workspace = true }
codex-core = { workspace = true }
codex-feedback = { workspace = true }
codex-file-search = { workspace = true }
codex-login = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
codex-shell-command = { workspace = true }
codex-state = { workspace = true }
codex-utils-approval-presets = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cli = { workspace = true }
codex-utils-elapsed = { workspace = true }
codex-utils-fuzzy-match = { workspace = true }
codex-utils-oss = { workspace = true }
codex-utils-sandbox-summary = { workspace = true }
codex-utils-sleep-inhibitor = { workspace = true }
codex-utils-string = { workspace = true }
color-eyre = { workspace = true }
crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] }
derive_more = { workspace = true, features = ["is_variant"] }
diffy = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "gif", "webp"] }
itertools = { workspace = true }
lazy_static = { workspace = true }
pathdiff = { workspace = true }
pulldown-cmark = { workspace = true }
rand = { workspace = true }
ratatui = { workspace = true, features = [
"scrolling-regions",
"unstable-backend-writer",
"unstable-rendered-line-info",
"unstable-widget-ref",
] }
ratatui-macros = { workspace = true }
regex-lite = { workspace = true }
reqwest = { workspace = true, features = ["json", "multipart"] }
rmcp = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
shlex = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
supports-color = { workspace = true }
tempfile = { workspace = true }
textwrap = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
"macros",
"process",
"rt-multi-thread",
"signal",
"test-util",
"time",
] }
tokio-stream = { workspace = true, features = ["sync"] }
toml = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
syntect = "5"
two-face = { version = "0.5", default-features = false, features = ["syntect-default-onig"] }
unicode-segmentation = { workspace = true }
unicode-width = { workspace = true }
url = { workspace = true }
webbrowser = { workspace = true }
uuid = { workspace = true }
codex-windows-sandbox = { workspace = true }
tokio-util = { workspace = true, features = ["time"] }
[target.'cfg(not(target_os = "linux"))'.dependencies]
cpal = { version = "0.15", optional = true }
hound = { version = "3.5", optional = true }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }
[target.'cfg(windows)'.dependencies]
which = { workspace = true }
windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_System_Console",
] }
winsplit = "0.1"
# Clipboard support via `arboard` is not available on Android/Termux.
# Only include it for non-Android targets so the crate builds on Android.
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { workspace = true }
[dev-dependencies]
codex-cli = { workspace = true }
codex-core = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
codex-utils-pty = { workspace = true }
assert_matches = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
insta = { workspace = true }
pretty_assertions = { workspace = true }
rand = { workspace = true }
serial_test = { workspace = true }
vt100 = { workspace = true }
uuid = { workspace = true }

View file

@ -0,0 +1,17 @@
▒▓▒▓▒██▒▒██▒
▒▒█▓█▒█▓█▒▒░░▒▒ ▒ █▒
█░█░███ ▒░ ░ █░ ░▒░░░█
▓█▒▒████▒ ▓█░▓░█
▒▒▓▓█▒░▒░▒▒ ▓░▒▒█
░█ █░ ░█▓▓░░█ █▓▒░░█
█▒ ▓█ █▒░█▓ ░▒ ░▓░
░░▒░░ █▓▓░▓░█ ░░
░▒░█░ ▓░░▒▒░ ▓░██████▒██ ▒ ░
▒░▓█ ▒▓█░ ▓█ ░ ░▒▒▒▓▓███░▓█▓█░
▒▒▒ ▒ ▒▒█▓▓░ ░▒████ ▒█ ▓█▓▒▓
█▒█ █ ░ ██▓█▒░
▒▒█░▒█▒ ▒▒▒█░▒█
▒██▒▒ ██▓▓▒▓▓▓▒██▒█░█
░█ █░░░▒▒▒█▒▓██

View file

@ -0,0 +1,17 @@
▒████▒██▒
██░███▒░▓▒██
▒▒█░░▓░░▓░█▒██
░▒▒▓▒░▓▒▓▒███▒▒█
▓ ▓░░ ░▒ ██▓▒▓░▓
░░ █░█░▓▓▒ ░▒ ░
▒ ░█ █░░░░█ ░▓█
░░▒█▓█░░▓▒░▓▒░░
░▒ ▒▒░▓░░█▒█▓░░
░ █░▒█░▒▓▒█▒▒▒░█░
█ ░░░░░ ▒█ ▒░░
▒░██▒██ ▒░ █▓▓
░█ ░░░░██▓█▓░▓░
▓░██▓░█▓▒ ▓▓█
██ ▒█▒▒█▓█

View file

@ -0,0 +1,17 @@
███████▒
▓ ▓░░░▒▒█
▓ ▒▒░░▓▒█▓▒█
░▒▒░░▒▓█▒▒▓▓
▒ ▓▓▒░█▒█▓▒░░█
░█░░░█▒▓▓░▒▓░░
██ █░░░░░░▒░▒▒
░ ░░▓░░▒▓ ░ ░
▓ █░▓░░█▓█░▒░
██ ▒░▓▒█ ▓░▒░▒
█░▓ ░░░░▒▓░▒▒░
▒▒▓▓░▒█▓██▓░░
▒ █░▒▒▒▒░▓
▒█ █░░█▒▓█░
▒▒ ███▒█░

View file

@ -0,0 +1,17 @@
█████▓
█▒░▒▓░█▒
░▓▒██
▓█░░░▒▒ ░
░ █░░░░▓▓░
░█▓▓█▒ ▒░
░ ░▓▒░░▒
░ ▓█▒░░
██ ░▓░░█░░
░ ▓░█▓█▒
░▓ ░ ▒██▓
█ █░ ▒█░
▓ ██░██▒░
█▒▓ █░▒░░
▒ █░▒▓▓

View file

@ -0,0 +1,17 @@
▓████
░▒▒░░
░░▒░
░██░▒
█ ░░
▓▓░░
█ ░░
█ ░
▓█ ▒░▓
░ █▒░
█░▓▓ ░░
░▒▒▒░
░██░▒
█▒▒░▒
█ ▓ ▒

View file

@ -0,0 +1,17 @@
████▓
█▓▒▒▓▒
░▒░░▓ ░
░░▓░ ▒░█
░░░▒ ░
░█░░ █░
░░░░ ▓ █
░░▒░░ ▒
░░░░
▒▓▓ ▓▓
▒░ █▓█░
░█░░▒▒▒░
▓ ░▒▒▒░
░▒▓█▒▒▓
▒█ █▒▓

View file

@ -0,0 +1,17 @@
█████░▒
░█▒░░▒▓██
▓▓░█▒▒░ █░
░▓░ ▓▓█▓▒▒░
░░▒ ▒▒░░▓ ▒░
▒░░▓░░▓▓░
░░ ░░░░░░█░
░░▓░░█░░░ █▓░
░░████░░░▒▓▓░
░▒░▓▓░▒░█▓ ▓░
░▓░░░░▒░ ░ ▓
░██▓▒░░▒▓ ▒
█░▒█ ▓▓▓░ ▓░
░▒░░▒▒▓█▒▓
▒▒█▒▒▒▒▓
░░

View file

@ -0,0 +1,17 @@
▒▒█ ███░▒
▓▒░░█░░▒░▒▒
░▓▓ ▒▓▒▒░░ █▒
▓▓▓ ▓█▒▒░▒░░██░
░░▓▒▓██▒░░█▓░░▒
░░░█░█ ░▒▒ ░ ░▓░
▒▒░ ▓░█░░░░▓█ █ ░
░▓▓ ░░░░▓░░░ ▓ ░░
▒▒░░░█░▓▒░░ ██ ▓
█ ▒▒█▒▒▒█░▓▒░ █▒░
░░░█ ▓█▒░▓ ▓▓░░░
░░█ ░░ ░▓▓█ ▓
▒░█ ░ ▓█▓▒█░
▒░░ ▒█░▓▓█▒░
█▓▓▒▒▓▒▒▓█

View file

@ -0,0 +1,17 @@
█▒███▓▓░█▒
▒▓██░░░█▒█░█ ▒█
██▒▓▒▒▒░██ ░░░▒ ▒
▓░▓▒▓░ ▒░ █░▓▒░░░▒▒
░▓▒ ░ ░ ▓▒▒▒▓▓ █
░▒██▓░ █▓▓░ ▓█▒▓░▓▓
█ ▓▓░ █▓▓░▒ █ ░░▓▒░
▓ ▒░ ▓▓░░▓░█░░▒▓█
█▓█▓▒▒▒█░▒▒░▒▒▓▒░░░ ░
░ ▒▓▒▒░▓█▒▓░░▒ ▒███▒
▒▒▒▓ ████▒▒░█▓▓▒ ▒█
▒░░▒█ ░▓░░░ ▓
▒▒▒ █▒▒ ███▓▒▒▓
█ ░██▒▒█░▒▓█▓░█
░█▓▓▒██░█▒██

View file

@ -0,0 +1,17 @@
▒▒▒█▒▒█▓░█▒
▒█ ▒▓███░▒▒█ █▓▓▒
▒▓▓░█ █▒ █ ▓▒ █▓▓▒ █
█░░█▓█▒ █ █▒░▒▓▒░▒▓▒▒▒█
▒▒▓▓ ▓░ ▒ █▒▒▓░▓░▒▒▓▒▒▒
▓▒░ ██░▓▒▒▒▓███░█▓▓▒▓░▓░
░░▒▓▓ █▓█▓░ ▒▓ █░▒░▒█
▒▓░░ ▒▒ ░░▓▒ ░▓░
▒ █▒▒▒▓▒▓█░░█░█▓▒█ ░█░░
▒▒▒░█▒█ ░░▓▒▒▒▒░░░▒▓░░▒ █
░▓░▒░ █████░ ▒▒▒▓░▓█▓░▓░
▒▒ █▒█ ░░█ ▓█▒█
▒▒██▒▒▓ ▒█▒▒▓▒█░
█░▓████▒▒▒▒██▒▓▒██
░░▒▓▒▒█▓█ ▓█

View file

@ -0,0 +1,17 @@
▒▒▒▒█░█▒▒░▓▒
▒█░░░▒▓▒▒▒▒█▒█░███
██▓▓▓ ░██░ ░█▓█░█▓▒
▓▓░██▒░ ▒▒▒██▒░██
░░▓░▓░ █░▒ ▓ ░▒ ░▒█
░▒▓██ ▒░█░▓ ▓▓ █▓█░
▒▒░░█ ▓█▒▓░██░ ▓▓▓█░
░░░░ ░▓ ▒░ █ ░ ░░░
░█░▒█▒▓▓▒▒▒░░░░██▓█░▓ ▒ ░░
▒▓▓█░▒█▓▒██▒█░█ ▒▒ ▓▒▒▒█▓▓░▒
█▒ ▓█░ ██ ▒▒▒▓░▓▓ ▓▓█
▒▒▒█▒▒ ░▓▓▒▓▓█
█ ▒▒░░██ █▓▒▓▓░▓░
█ ▓░█▓░█▒▒▒▓▓█ ▓█░█
░▓▒▓▓█▒█▓▒█▓▒

View file

@ -0,0 +1,17 @@
▒▓▒▓▒█▒▒▒██▒
▒██▓█▓█░░░▒░░▒▒█░██▒
█░█░▒██░█░░ ░ █▒█▓░░▓░█
▒░▓▒▓████▒ ▓█▒░▓░█
█▒ ▓█▒░▒▒▒▒▒ ▒█░▒░█
█▓█ ░ ░█▒█▓▒█ ▒▒░█░
█░██░ ▒▓░▓░▒░█ ▓ ░ ░
░ ▒░ █░█░░▓█ ░█▓▓░
█ ▒░ ▓░▒▒▒░ ▓░█████████░▒░░█
▒▒█░ ▓░░█ ▓█ ░▒▒▒▒▒▒▓▓▒▒░█▓ ░
▒▒▒ █ █▒▓▓░█ ░ ███████ ░██░░
█▒▒▓▓█ ░ ██▓▓██
▓▒▒▒░██ █▒▒█ ▒░
░░▒▓▒▒ ██▓▓▒▓▓▓▒█░▒░░█
░████░░▒▒▒▒░▓▓█

View file

@ -0,0 +1,17 @@
▒▒█▒░░▒█▒█▒▒
█▓▒ ▓█▒█▒▒▒░░▒▒█▒██
██ ▒██ ░█ ░ ▒ ▒██░█▒
▒░ ▒█░█ ▒██░▒▓█▒▒
▒░ █░█ ▒▓ ▒░░▒█▒░░▒
▓░█░█ ███▓░ ▓ █▒░░▒
▓░▓█░ ██ ▓██▒ █▒░▓
░▒▒▓░ ▓▓░ █ ░░ ░
░▓░░▓█▒▓▒▒▒▒▒▒▒██▓▒▒▒▒█ ▓ ░▒
█░▒░▒ ▓░░▒▒▒▒░▒ █▒▒ ░▒▒ █▓ ░░
▒█▒▒█ █ ▒█▒░░█░ ▓▒
█ ▒█▓█ ▒▓█▓░▓
▒▒▒██░▒ █▓█░▓██
▒█▓▓ ░█▒▓▓█▓ ░ ░█▓██
░██░▒ ▒▒▒▒▒░█

View file

@ -0,0 +1,17 @@
▒▒█▒█▒▒█▒██▒▒
███░░▒▒█░▒░█▓▒░▓██▒
▓█▒▒██▒ ░ ░▒░██▒░██
██░▓ █ ▒█▓██▓██
▓█▓█░ █░▓▒▒ ▒▒▒▒█
▓ ▓░ ███▒▓▓ ▒▒▒█
░█░░ ▒ ▓░█▓█ ▒▓▒
░▒ ▒▓ ░█ ░ ░
░ ░ ██▓▓▓▓▓███ ▒░█ ░█ ▓▓ ░
░ ░▒ ░▒ ▒█░ ▒ ░█░█ ▓ ▓▓
▓ ▓ ░░ █░ ██▒█▓ ▓░ █
██ ▓▓▒ ▒█ ▓
█▒ ▒▓▒ ▒▓▓██ █░
█▒▒ █ ██▓░░▓▓▒█ ▓░
███▓█▒▒▒▒█▒▓██░

View file

@ -0,0 +1,17 @@
▒██▒█▒▒█▒██▒
▒█▓█░▓▒▓░▓▒░░▓░█▓██▒
█▓█▓░▒██░ ░ █▒███▒▒██
▓█░██ ██░░░▒█▒
▒░░▓█ █▒▓░▒░▓▓▓█░
▒░█▓░ █░▓░▓▒▓░ ▒░▒▒░
░██▒▓ ░█░▒█▓█ ░░▓░
░░▒░░ ░▒░░▒▒ ░▒░ ░
░░█ █ █░▒▒▓▓▓▒██▒▒█░▒ ▒█ ▒░▓
▒░▒ █▒▒▒█ ▓█ ░▓▓░ ▒█▓▒ ░██ ▓▒▒
▒▒▒▒░ ██ ░ ░▓██▒▓▓▓ █░
▒█▒▒▒█ ▒██ ░██
█ █▓ ██▒ ▒▓██ █▒▓
█▓███ █░▓▒█▓▓▓▒█ ███
░ ░▒▓▒▒▒▓▒▒▓▒█░

View file

@ -0,0 +1,17 @@
▒██▒▒████▒█▒▒
▒▒░█░▒▒█▒▒▒█░▒░█░█▒
█ █░██▓█░ ░▓█░▒▓░░█
▓▓░█▓▓░ ▒▓▓▒░░▓▒
▓▓░░▓█ █▓████▓█▒░▒
█▒░ ▓░ ▒█████▓██░░▒░█
░░░ ░ ▓▓▓▓ ▒░░ ░██
░▓░ ░ ░ ░█▒▒█ ░ █▓░
▒ ▒ ░█░▓▒▒▒▒▒▓▒░▒█░▒ ▒▒ ░ ░░░
░▒▒▒░ ▒ ▓░▒ ▒░▒▒█░ ▒▒░
▓█░ ░ ░ █░▓▓▒░▒▓▒▓░
█░░▒░▓ █▓░▒▒▓░
▒ ░██▓▒▒ ▒▓ ▓█▓█▓
▒▒▒█▓██▒░▒▒▒██ ▓▒██░
░ █▒▒░▒▒█▒▒██░

View file

@ -0,0 +1,17 @@
▒░▒▓███▒▒█▒
█ ▒▓ ░▒▒░▒▒██▒██
█ █▓▒▓█ ░ ▓░▓█░███ ▒
██▓▓█▓░▒█▒░░▓░ ▒█▒░▒▒█
█ ▓▓▒▓█ ░ ▓▒▒░░░▒░██
░█▒█▒░ ███▓ ▓░▓ ▓ ▒
░ ░░ █▓▒█▓ ▓▒▒░▒▒░▒
░ ▒░░ ░█▒▓▒▒░░▒▓▓░░░
░▓ ░▓▓▓▓██░░░██▒██▒░ ░ ░░
▒ ▓ █░▓██▓▓██░▓▒▒██░ ░█░
▒ █▒░▒█ ░ ▒█▓█▒░▒▓█░
▒ ▒██▒ ░ ▓▓▓
▒▓█▒░░▓ ▒▒ ▒▓▓▒█
▓▓██▒▒ ░░▓▒▒▓░▒▒▓░
█▓▒██▓▒▒▒▒▒██

View file

@ -0,0 +1,17 @@
▒█▒█▓████▒
█ ███░▒▓▒░█░░█
▓░▓▓██ ▓░█▒▒▒░░░▒
░██░ ▓ ▒░ ▒░██▒▓
█▒▒▒█▓█▒▓▓▒░ ░▓▓▒▓█
▒█░░░▒██▓▒░▓ ▓░█░▓▓░█
░▓░█░ ░▒▒▓▒▒▓░▒▓▒ ░▒░
░░░▓░▓ ░▒▒▒▓░▒▒░▒░░▒
▒█▒░ ░▒▒▒▒▒▒█░░▒▒░██░▒
▓▓ ░▓░█░▒░░▓█▒░▒█▒▓▒░
▒░█▓▒░░ ██▓░▒░▓░░
░▒ ░▓█▓▒▓██▓▒▓█▓▓░▓
▒░▒░▒▒▒█▓▓█▒▓▒░░▓
▒▓▓▒▒▒█▒░██ █░█
░█ █▒██▒█░█

View file

@ -0,0 +1,17 @@
▒▓███ ██
▓█░▓▓▒░█▓░█
▓█ ░▓▒░▒ ▒█
▓█ █░░░▒░░▒█▓▒
░▒█▒░▓░ █▒▓▓░▒▓
▒ ░▓▓▓ █▒▒ ▒▒▓
░ ██▒░░▓░░▓▓ █
▓▓ ▒░░░▒▒▒░░▓░░
░ ▓▒█▓█░█▒▒▓▒░░
▓▒░▓█░▒▒██▒▒█░
░░ ▓░█ ▒█▓░█▒░░
▒▒░░▓▒ ▓▓ ░░░
█ █░▒ ▒░▓░▓█
░ █▒▒ █▒██▓
▒▓▓▒█░▒▒█

View file

@ -0,0 +1,17 @@
▓█████
░▓▓▓░▓▒
▓█░ █░▓█░
░░░▒░░▓░░
░ ░░▒▓█▒
░▒▓▒ ░░░░░
▒ ░░▒█░░
░ ░░░░▒ ░░
░▓ ▓ ░█░░░░
█▒ ▓ ▒░▒█░░
░▓ ▒▒███▓█
░░██░░▒▓░
░▒▒█▒█▓░▒
▒▒▒░▒▒▓▓
█▒ ▒▒▓

View file

@ -0,0 +1,17 @@
▓██▓
░█▒░░
▒ ▓░░
░▓░█░
░ ░░
░ ▓ ░
▒░░ ▒░
░▓ ░
▓▒ ▒░
░░▓▓░░
░ ▒░
░▒█▒░
░▒█░░
█▒▒▓░
░ ▓█░

View file

@ -0,0 +1,17 @@
██████
█░█▓ █▒
▒█░░ █ ░
░░░░▒▒█▓
▒ ░ ░ ░
░█░░░ ▒▒
░▒▒░░░ ▒
░░▒░░
░░░█░ ░
▒░▒░░ ░
█░░▓░▒ ▒
░▓░░░ ▒░
░░░░░░▒░
░▒░█▓ ░█
░░█ ▓█

View file

@ -0,0 +1,17 @@
▒▓▒▓██▒▒▒▒█▒
▒██▓▒░░█░ ▒▒░▓▒▒░██▒
█▓▓█▓░█ ░ ░ ░ ███▓▒░█
▓█▓▒░▓██▒ ░▒█ ░░▒
█▓█░▓▒▓░░█▒▒ ▒▒▒░░▒
▓░▒▒▓ ▓█░▒▓▒▒ ░ ▒▒░
▒█ ░ ██▒░▒ ░█ ▓█▓░█
█▓░█░ █▓░ ▓▒░ ░▒░▒░
▓ █░ ▓░██░░█▓░▒██▒▒▒██▒░▒ ▓░
█▒▓▒█ ▓▓█▓▓▓░ ░█░▒▒█ ▒▓█▓▒░░▒░░
█▒░ ░ ░░██ ███ ███▓▓▓█▓
██░ ▒█ ░ ▓▒█▒▓▓
▒▒▓▓█▒█ ██▓▓ █░█
▒▒██▒██▒▒▓▒▓█▓▒█▓░▒█
░███▒▓░▒▒▒▒░▓▓▒

View file

@ -0,0 +1,17 @@
▒▓ ████
▒▓▓░░▒██▒▒
█▒░█▒▒░██▒
░░▒░▓░▒▒░▒ ▒█
▒█░░░▒░█░█ ░
░█░▒█ █░░░░▓░
▒▓░░░▒▒ ▒▓▒░ ▒░
░ ██▒░█░ ░▓ ░
░▒ ▒░▒░▒▓░█ ░
░░▒░▒▒░░ ██ ░
▒░░▓▒▒█░░░█░░
░█▓▓█▓█▒░░ ░
▒░▒░░▓█░░█░▓
█▒██▒▒▓░█▓█
▒▓▓░▒▒▒▓█
░░░░

View file

@ -0,0 +1,17 @@
▒▓▓████▒█
▒██▓██▒ █▒███
█░▒▓▓█▒▒░▓ ░▒█▒
█░▓█▒▒█▓▒█▒▒░▒░░▒
▒░░░░█▓█▒▒█ ▒░▓▒▒
▓░▒░░▒░█ ▒▓██▓▓░█ ░
▓░░ ░▒█░▒▓▒▓▓█░█░▓░
▒▒█ ░░ ░▒ ░▒ ░░▒▓░
░▒█▒░█▒░░░▓█░░░▒ ░
░░░▓▓░░▒▒▒▒▒░▒░░ █
▒█▒▓█░█ ▓███░▓░█░▒
░░░▒▒▒█ ▒▒█ ░
▓░█▒▒ █ ▓ ░█░▓░
▓░▒░▓▒░░█░ █░░
█ ▒░▒██▓▓▓█
░░░░

View file

@ -0,0 +1,17 @@
█████▓▓░█▒
▓█░██░▒░░██░░█
▓▒█▒▒██▒▓▓░█░█▒███
█▓▓░▒█░▓▓ ▓ █▒▒░██ █
▓▓░█░█▒██░▓ █░█░▒▓▒█▒█
▒▓▒▒█▒█░░▓░░█▒ ░█▓ █
█░ ▓█░█▒░░██░█▒░▓▒▓▓░█▒
░░░█▒ ▒░░ ▓█░▓▓▒ ▒░ ░
▒░░▓▒ █▒░ ▒▒░███░░░▒░ ▒░
█ ▒░░█▒█▒▒▒▒▒▒░░█░▓░▓▒
█▒█░░▓ ░█ ███▒▓▓▓▓▓▓
▒█░▒▒▒ █▒░▓█░
███░░░█▒ ▒▓▒░▓ █
▒▓▒ ░█░▓▒█░▒█ ▒▓
░▓▒▒▒██▓█▒
░░░

View file

@ -0,0 +1,17 @@
▒██▒█▒█▒░▓▒
▒██░░▒█▒░▓░▓░█░█▓
▒▓▒░████▒ ░ █▓░░█ █
█▒▓░▓▒░█▒ █░░▒▒█
▒▓░▓░░░▓▒▒▒ ░█▒▒▒
▓▓█ ▒▒▒▒░▒█ ▓▒▓▒▒
░░█ ▒██░▒░▒ ░█░░
█░██ ███▒▓▒█ ▒ ░█
░░░ ░ █░ ▓████▓▒▒█░░█▓▒░▒░
▒▓░█ ▓▓█▓░░░▒▒▒▒▒░░█▒▒▒░░▓
▒▒▒█ ░▓░▓ ▓ ███ ░░█▓▒░
▒█▒██ █ ▓▓▓▓▒▓
█▒ ███▓█ ▒█░█▓█▒█
▒░ █▒█░█▓█▒ ▓█▒█░█
▒▒██▒▒▒▒██▓▓
░░░░

View file

@ -0,0 +1,17 @@
▒█▒████▒░█▒
▒███▓▒▓░ ░██▒██▓█▒▒
▒▓▓█░█ ▓░█░ ░▒▒▒█ ███
█▓▒░█▒▓█▒ █░██▒▒
▓▓░▒▓▓░ ░ █ ▒▒█▒▒
█▓▒░░▓ ▒▒ ░▒█▒ ▒█▒░▒
░█▒░▒ █▒▒█░▒▒ ░▓░▒
▒░▒ ▓ ░█▒░▓ ░ ▓ ▒▒
██▓▓ ▓▒▓▓ ▒▒▒██████░▒▒ ░▒░
░░▒█▓██▒ ▓▓█░░░▒░▓▒▒▒█▓▒░░░░▒
▓▒▒█ ░▒░█▒ ██░░░░▒ █▓█▒░█
▓█▒▓▒▒▒ ▓▓▓░▓█
▒█░░█▒▓█ ▒█▒ ▒▓█░
▓▒▓░ ░██▓██▒█▒█░██▓█
░▒▓▒▒▒▒▒▒▓▒█▒▒
░░░

View file

@ -0,0 +1,17 @@
▒██▓▒███▒██▒
██▒█▓░███ ░█░▓ ░█▒▒
▒▓▓░▓██░▒█ ░ ░ █▒█▓ ░██
█▓▓█▓█▓█▒ ██▒▒░▒
▓▓░░▓▓▒ ▒██ ░▒█░█
▓▓▓▓█░ █░▒ ▓▓█▒ ░▒▒░
▒ ▓▓ ▒▒ ██▒▓ ░▒▒▒
░░░▓ ▓▒▒▓▓█ ▓ ▓
▓ █▒ █░░▓▓ ▓░▒▒▒▓▒▒█░░ ░░▒█
░█▒▓█ ▓▓▓ ██▓░▓ ▒█▒▒▒▒▓ ░▓█ ░█
▓░▒██▓▒▒░▓▒░ ░ ▒▒▒▒█▒▒█▓▓▒█░
▓▒▒▓░ ▒▓█ █▒
▒▓░▒▓█▓█ █▓▓▒███
▒▒ ░█░▓▓░░█░▓▓█ ▒▓▓
▒░▓▒▒▒▓▒▒███ ▒

View file

@ -0,0 +1,17 @@
▒▒█▒████▒██▒
▒▒ ▒█▓▓▓█▒█▓██ ███▒
█▒█▒███▓█ ░░ ░ █░██░██░█
▒░ ██▒▒▒▒ ██░▒ ░
█▓▒▓▒█░▒░▒█▓ ▒▒▓█
▓ █▓░ █▒ ░▓█ ▒▒█
░ ▓ ░ ▒ ▒▒ ░▒░█
░░▒░ ▒▒ ▒▓▓ ▒░ ░
░█ ░ ▓▓ ██ ████▒█████▒ ░▒░░
▒█░▒ █░▒▒▓░▓ ░░▒▒▒▒▒▒▒░░ ▒▓█░
█ █░▒ █▒█▓▒ ██▒▒▒▒▒ ░█ ▓
██ ▒▓▓ █▓░ ▓
▒▓░░█░█ ███ ▓█░
██▒ ██▒▒▓░▒█░▓ ▓ █▓██
░██▓░▒██▒██████

View file

@ -0,0 +1,17 @@
▒▓▒▓█▒▒█▒██▒
▒▓ ██░▓ ░▒▒▓█▓░ ▓██▒
██▒░░░██ ░ ░▒░▒▒█░▓▒▒▒
▓▓░█░ ▓██ ░██▒█▒
▓░▓▒░▒░▒▓▒░█ ▒ ▒░█▒
▓░░▒░ █▒░░▓█▒ █ ▒▒░█
▒░▓░ ███▒█ ░█ █ ▓░
░▓▒ █░▓█▒░░ ░░░
▒░ ░▒ ▓░▓ ▒▓▓█░███▒▒▒▒██ ░░█
░▒▓ ░ █▓▓▓█▒░░▒▒░█▓▒█▓▓▒▓░▓▓ ░
░░▓█▒█▒▒█▒▓ ████████▒▓░░░░
█░▒ ░▒░ █▒▓▓███
▒▒█▓▒ █▒ ▒▓▒██▓░▓
░░░▒▒██▒▓▓▒▓██▒██▒░█░
█▒▒░▓░▒▒▒▒▒▓▓█░

View file

@ -0,0 +1,17 @@
▒▓▒▓▓█▒▒▒██▒
▒█ █▓█▓░░█░▒█▓▒░ ██
█▒▓▒█░█ ░ ▒▒░█▒ ███
█░▓░▓░▓▒█ ▓▒░░░░▒
█▒▓█▓▒▒█░▒▒█ ░ ▒░▒░▒
░░░░▓ ▒▒░▒▓▓░▒ █▓░░
░▓░ █ ░▒▒░▒ ░█ ██░█░█
░▓░▒ █▒▒░▓▒░ █░▒░
░█░▒█ ▓▒░ █░█▒▒░█░▒▒▒██▒ ░▓░
▒▒░▒██▓██ ░ ▓▓▒▒▒█▒▓█▓░▓█░░
▒█░░█░█▒▒▓█░ ██ █░▓░▒▓
▒▒█▓▒▒ ░ ▓▒▓██▒
▒▓█▒░▒█▒ ▒▒████▓█
▒░█░███▒▓░▒▒██▒█▒░▓█
▒▓█▒█ ▒▒▒▓▒███░

View file

@ -0,0 +1,17 @@
▒▓▒▓▓█▒▒██▒▒
█▒▓▓█░▒██░██▓▒███▒
███░░░█ ░ ░▓▒███▓▒▒
▓█░█░█▒▒█ ▒█░░░░█
█▒░░░█▒▒██▒ ▓▒▒░▒█
▓▓▓░▓░▒█▓░▒▒░█ ▓▒▒▓░
▒ █░░ ▒▒░▓▒▒ ▒█░▒░
░ ░░░ ▒░▒░▓░░ ░█▒░░
▒▓░▓░ ▓█░░█▓▓█▒░█░▒▒██▒▓▒▓░
░░▒█▓▒▒▒▓█ ░▓▒██░░█▓▒▒▒░█░▒
▓ ░ ▓░░░▓▓ █ ██ ░▒▒▓░
█ ▓ ▓█░ █▓▒▓▓░░
▓░▒▒███ ▒█▒▒▓███
░ ░██ █ ▓░▒▒████ ▓▓█
▒▓▓███▒▒▒░▒███

View file

@ -0,0 +1,17 @@
▒▓░▓██▒▒██▒
██░█▒░███▒▒▒▓ ░██
█ █░░░█░ ░▒░░ █▓▒██
▒▒░░░░▓█ ▒░▒█░▓█
░█░█░░▒░▓▒█ ▓ █░░▒
░ ▓░░ ░█▒▓░▒ █▓░░░
░▒ ░ ▒▒░▒░▒░ ██▒░░
▒ ▓░░ ▒█▓░█░░ █ ░░░
▓ ░█ █ ▒▓░▒▓░░▓▓▒░░▒▓█▒░░
░██░░▒▓░░▓█░▓▒░░▒▒█▒█▓▒░▒░
▒ ▒▒▓█░█▒▓ ██████ ▒▓░░
█▒ ▓▒▓▒░ █ ▓▓▓▓█
█▓██▒▒▒▒ █▒░██▓██
▒▒█▒░█▒▓░▒▒▒██░██▓
░█ ░▓░▒▒█▒▓██

View file

@ -0,0 +1,17 @@
▒▒█▒▓██▒██▒
█ █▓░░░█▒▒ ░ █
▒░▒█░▓▓█ █ ░▓░█▒█▒█
▒█▒█▓░██░ █ ▒▒░░▒
█ ▓░▓█▒░▓▒ ▓█▒░░█
░██░▒▒▒▒▒░▒█ ▒█░░░
░█░░░ █▒▓▒░░░ ░▒░▓░█
▒█░░▓ ░█▒▓░██▓ ▓░▓░░
▒ ▒░░▒▒ ▓█▒░░▓█████▒░░░
▒█▓▒▒░ █░█░░▓░▒▒▒░░▒█
▓▓▒▒░▒░░░▓█▒█▒█ ▒█ ▓▒░
██ ░▒░░░ ▓█▓▓▓█
█▒▒█▒▒▒▒ ▒▓▒▒░█▓█
▓▓█░██ ▓▓██▓▓▒█░░
░░▒██▒░▒██▓▒░
░░

View file

@ -0,0 +1,17 @@
▓▒▒█▓██▒█
▓█▒▓░░█ ▒ ▒▓▒▒
▓ █░░▓█▒▒▒▓ ▒▒░█
░░▓▓▒▒ ▒▒█░▒▒░██
▓█ ▓▒█ ░██ █▓██▓█░░
░ ░░░ ▒░▒▓▒▒ ░█░█░░░
░ ░█▒░██░▒▒█ ▓█▓ ░░░
░ ░▓▒█▒░░░▒▓▒▒▒░ ░░
█░ ▓░ ░░░░█░░█░░░
░▒░░░▒█░▒░▒░░░░▒▒░░░
░▒▓▒▒░▓ ████░░ ▓▒░
▒░░░▒█░ █▓ ▒▓░░
▒█▒░▒▒ ▓▓▒▓░▓█
▒▓ ▒▒░█▓█▒▓▓█░░
█▓▒ █▒▒░▓█▓

View file

@ -0,0 +1,17 @@
eoeddccddcoe
edoocecocedxxde ecce
oxcxccccee eccecxdxxxc
dceeccooe ocxdxo
eedocexeeee coxeeo
xc ce xcodxxo coexxo
cecoc cexcocxe xox
xxexe oooxdxc cex
xdxce dxxeexcoxcccccceco dc x
exdc edce oc xcxeeeodoooxoooox
eeece eeoooe eecccc eccoodeo
ceo co e ococex
eeoeece edecxecc
ecoee ccdddddodcceoxc
ecccxxxeeeoedccc

View file

@ -0,0 +1,17 @@
eccccecce
ccecccexoeco
eeoxxoxxoxceoo
xeeoexdeoeocceeo
o dxxcxe cooeoxo
xe cxcxooe eecx
e xcccxxxxc xoo
c xxecocxxoeeoexx
c xe eexdxxcecdxx
x oxeoxeoeceeexce
o cxxxxxcc eocexe
eecoeocc exccooo
xc xxxxcodooxoe
deccoxcde ooc
co eceeodc

View file

@ -0,0 +1,17 @@
occcccce
oc dxxxeeo
oceexxdecoeo
xeexxddoedoo
ecodexcecdexxo
xcexxceddxeoxx
cc oxxxxxxexde
x xxoxxeo xcx
o cxoxxcocxex
cc exodocoxexe
ceo xxxxdoxeex
eeooxecoccdxe
e cxeeeexdc
ec cxxoeoce
ee cccece

View file

@ -0,0 +1,17 @@
ccccco
odeeoxoe
c xoeco
ocxxxddcx
x cxxxxoox
xcoocecexc
x xoexxe
x ocexxc
co xoxxcxx
x oxcdce
xo xcdcco
o cx eox
o ccxocex
ceocoxexe
e cxeoo

View file

@ -0,0 +1,17 @@
occco
xeexx
xeexc
xccxe
c xx
cdoxx
o xx
c cx
oc exo
xc cdx
ceoo xe
xeeex
xcoxe
ceexd
o ocd

View file

@ -0,0 +1,17 @@
ccccd
ooeeoe
xexxo x
xxoxcexo
xxxe x
xcxx cx
xxxx o c
xxexe e
xxxx c
ceoo do
exccooox
xcxxeeex
o cxddde
xeoceeo
ec cdo

View file

@ -0,0 +1,17 @@
cccccxe
eodxxedco
ooxcdexccx
xoe ooooeex
xxdcdexxocex
exxoxxoox c
xx xxxxxxox
xxoxxcxxx cox
xxcoocxxxeodx
xexdoxexco ox
xoxxxxex e d
xccoexxeo d
cxeo oooe de
xexxeeoceo
eeceeeeo
ee

View file

@ -0,0 +1,17 @@
edcccccxe
oexxcxxexde
xooceodexx ce
ooo dceexexxccx
xxdeoccdxxcoxee
xxxcxc xed x xox
eex oeoxxxxocco x
xod xexxoxxxcd ex
eexxxcxoexxccc o
cceeoddecxoex oex
xxxcccocexdcdoxxe
xxc xe eooo o
exc x oooeox
exxcecxoocex
cdoeddeedc

View file

@ -0,0 +1,17 @@
odcccddxoe
edccxxxcdcxoceo
oceoeddecocxxxece
oxoeoxcee cxdexxxde
xoe x xcoedeoo o
edcooe odox oodoxoo
c dox oooxe ccxxodx
ocdx ooxxoxoxxddc
oocoeddcxeexeedexxx x
xcedeexoceoxxe eccce
eeeoccccccceexcooe ec
exxec eoxxe d
eee cee ocooeeo
o xccdeceedcdxc
ecdoeocxcecc
e

View file

@ -0,0 +1,17 @@
eddcddcdxoe
eccedoccxeeoccdde
eodxcccdcocoeccooe c
oxxcooecc ceeeodxedeeeo
eeoo ox ecceeoxoxeedeee
oex ooxoeeeoocoxcooeoeox
xxedo cocoxceoccxdxdo
ceoxx eecxxde xdxc
ecc oedddddcxxoxcoeo xcxe
eeexcec xxoeeeexxxedxee o
xoxeeccccccce eeeoxocoeoe
ee oeo eeccocec
eecceeo eceeoeoe
cxoccccdddecceoeoc
cxxeoeeooccdcc
e

View file

@ -0,0 +1,17 @@
eeddcxcddxoe
ecxxxeodddeceoxcoo
ocddocxcce ecdoecde
odxcoee eddcoexco
xxoeoe oxecocxe xeo
xeocc excxo oo cocx
edxxc oceoxcoe odocx
xxxx xdcexco x xxx
xcxeoddddddxxxxccdcxd e cxx
edooxdcoecceoeo ee deeeoooxe
cecocxcccccccc eeeoxoo ooc
eeecee eooeooc
c eexxco oddooxde
ccoxcoxceeddocc dcxc
cxoedoceooecoe

View file

@ -0,0 +1,17 @@
eoeddcdddcoe
ecoocdcxxxdxxdecxcce
oxcxeccxcee eccdcoxxdxo
exoeoccooe ooexoxo
oecocexeeeee eoxexo
cocce xcecoec eexcx
oxccx eoxdxexo ocxcx
xc ee oxcxxdc xcoox
cccdx dxeeexcoxccccccccoxexxc
edcx oxxc oc xdeeeeeooeexco x
eee c ceooxc ecccccccccxocxx
ceeooo e ocdooc
oeeexco odec exc
exedeecccdddddodceexxc
eccccxxeeeexdocc

View file

@ -0,0 +1,17 @@
eecdxxdcdoee
oddcdoeodddxxeececo
oocecccxcc ecececcxce
excecxc eocxeocee
ex oxc eo exxecexxe
oeoxc cccdxco cexxe
dxdcx oc occe oexo
xeeoe ccddxco xxcx
xoxxdoddddddddeocdeeeec o xe
cxexec oeeeeeexe ceecxde oo xx
eoeecccccccccc eodxxox oe
c ecoo eocoxo
eeecoxe odcedcc
eooocxceddodcxceoocc
eccxe deeeexccc

View file

@ -0,0 +1,17 @@
eeodcddcdcoee
occeeeecxdxcdeeocce
dceeccece eexcceeco
ocxdcc eodcodco
oooce oxoee eeeeo
ocox occeoo eeeo
xcxe e oeooc edec
ee ed cxo x x
x x ocdddddccc exocxo do x
x xe xe eox ececxo ocoo
d co eeccc ce cceod oe o
cc dde ecc o
ce eoe eodcc oe
cde ccccdxxdddccc oe
cccdceeeeoedcce

View file

@ -0,0 +1,17 @@
eocdcddcdcoe
ecocxoeoxoexxdxcocce
odcdxecce ecceccceeco
dcxccc ooxxxece
exxoc oeoxdxoodcx
excoe oxoxdeoe exedx
xcceo xcxecoc xxox
xxdxe xexxee xexcx
xxoco cxddddddcceecxe eo exdc
exd ceeeo oocxoox ecdecxoo oed
eeeex cccccccce edcceooocoe
eceeeo ecocxoc
cccd cce eococceo
cdccoccxddcddodccccoc
cxcxedeeeodeodce

View file

@ -0,0 +1,17 @@
eocedccccdcee
edxcxeeoeddoxexcxce
occxcodce cxdcxedxxo
odxcdoe eddexxde
ooxeoc ooooccocexe
oexcoe ecccccoccxxexo
exxcx odoo exe c xcc
xox x xcxoeeo x cox
ece xcxddddddddxecxecee x xxx
xeeexcdc oee exeeox eex
ocx x eccccccc ceoddxeoeoe
oxxexo ooxeeoe
e xocoee eocdcoco
edecdccexddecccoecce
cx cdexeeceecce

View file

@ -0,0 +1,17 @@
exedcccddoe
oceocxeexddcoecc
occdeoccx oedcxcco e
ocooooxdoeexoe ecexeec
o ooeoo eccoeexeeexoc
xoecee cooo oxd oce
x xx ooeoocoeexeexe
x exx xodoeexxeooexx
xo xddddccxxxccecoex x xx
e o cxoooddooxoeeccx xcx
e cexeccccccce eoocexdooe
e eoce x codo
eoceexo edceodec
oocoeecxxddddxeeoe
cdeccdeeeddcc

View file

@ -0,0 +1,17 @@
ecdcdcccce
o coceedexcxxo
oxoooocoxcedexxxe
xccx o dx cexoceo
oeeeoocedoexc xooeoc
eoxxxeccoexd oxoxooxo
xoxcx xeeoeeoxeoecxdx
xxxoxoc xedeoxeexdxxe
ecexcxeeddddcxxeexccxe
oocxoxoxexxdcexecdoex
excoexecccccccoxexoxe
xecxdcdeoocdeooooxo
eeexeeecdooeoexxo
eodeeecdxcc cxc
xoccecoecxc

View file

@ -0,0 +1,17 @@
edccccco
ocxdoexcdxo
occcxdexecceo
dccoxxxexxecoe
xeoexoxcceodxed
e cxodocceeceeo
x ccdxxoxxddcc
oo exxxeedxxoxx
x oecdcxcddoexx
oexooxeeoceecx
xecoxcceooecexx
eexxoe oocxxe
c cxe eeoxoo
xcceecceccd
eodecxeec

View file

@ -0,0 +1,17 @@
dcccco
xddoxoe
dce cxocx
xxxexxdxx
x exeocd
xeoecexxxe
d cxxecxx
x exxxdcxx
xo o xcxxxx
cd ocexecxx
xo eecccoc
xxccxxeox
xddcdooxe
eeexedoo
cec eeo

View file

@ -0,0 +1,17 @@
occd
xcexe
d dxe
xoecx
x xx
x ocx
exx ex
xoccx
oe ex
xxodxx
x ex
xdcdx
xdcxx
ceeox
x ocx

View file

@ -0,0 +1,17 @@
ccccco
oxco ce
eoxx ccx
xxxxeeoo
e xcx x
xoxxx ee
xeexxx e
xxdxx
xxxcx e
exdxx e
cxxoxe d
xoxxx ex
xxxxexex
xdxcocxc
xxc oo

View file

@ -0,0 +1,17 @@
eoddccddddoe
ecooexxcxcddxdeexcce
odocdxccce ecx cccoexo
ocoexdoce edc xxe
cocxoeoxxcee eeexxe
oxeeo ooxedee x eex
dc x ccexecxo ocoxo
ooxox ooxcoex xexdx
occx dxccxxcoxdcceeeccexecdx
oedeo oocoddx xcxeeo doodeexexe
cex x cxxcoc cccccccccoooooo
ccx ec e oeceoo
deooceo ocdocoxc
decoecceddddoddcdeecc
ecccedxeeeexdoec

View file

@ -0,0 +1,17 @@
edcccco
eodxxeccde
ccexoeexcoe
xxexoxeexe eo
dcxxeexoxo x
xcxec cxxxxox
eoxxxee eoex de
cx ccdxoxcxo e
cxecexdxeoxo e
cxxexeexx co e
exxdeecxxxcxx
xcoooocexxc x
exexxocxxoxo
oeocdeoxooc
eooxeeedc
eeee

View file

@ -0,0 +1,17 @@
eodccccdo
eccdcoeccecco
oxeooodeeocxece
oxoceecdeoeexexxe
exxxxcoceeocexoee
dxeexexccedcoooxocx
oxx xecxeododcxcxox
eeo xxcxe xeccxxeox
xeoexcexxxocxxxe x
cxxxooxxeeeeexexx c
eceocxo occceoxcxe
xxxeeeo edc x
dxcde o o xceoe
dxexoexeoxcoxe
ccdxeccoodc
eeee

View file

@ -0,0 +1,17 @@
occccddxoe
dcxccxexxccxxo
oecdeocedoecxcecco
cooxeoedo o oeexco o
ooxoxceccxd ceoxeoeceo
eoeeoecxedxxce xco c
cxcdoecexxooxodeoeooxce
xxxoe cexxcocxdoecexcce
exxoe cexceexcccxxxdxcde
ccceexceceeeeeexxcxdxoe
oecxxo xccccccedooooo
eoxeee oexocx
cccxxxce eoexo o
eoecxcxddceecceo
xddeeococecc
eee

View file

@ -0,0 +1,17 @@
eocdcdcdxoe
eccxxecdxdxoxcxco
eoexcccodce ccoxxcco
oeoxoexoe cxxeec
eoxoexxoeee xceee
ooo eeeeeeo oeoee
xxc eocxexe xcxx
cxoo occeodo ecxc
xxx x oe ocooodddcxxcoexex
eoxccodooexxeeeeexxceeexxo
edeo xoxo o ccccccc xxooee
ececoco oododo
ceccocdo ecxoocec
exccecxodcecdoecxc
cddcoeeeeccdoc
eeee

View file

@ -0,0 +1,17 @@
eodccccdxoe
ecccoedxcxccdccdode
eoooxccdxce eeeeoccco
ooexceooe cxocee
ooxeoox xc o eecee
ooexeo eecxece eoexe
xcexe ceecxee xdxe
exdcd xcexocx o ee
ocooc oeooceddccccccxeec xee
xxeooooecoocxxxexoeeeooexxexe
oeeo xexce ccceeeee oooexc
ooeddee odoxoc
ecexcedo ecdceooe
oeoxcxcodocdcdceccdc
cxeddeeeeeddcde
eee

View file

@ -0,0 +1,17 @@
eocddcccdcoe
ocdcoxccccxcxdcxcde
eooxdccxecce eccdcocxco
ooocoodoe cceexe
ooxxooe ceco xecxo
dodoce cxecooce xeex
e oo ee cceo xeee
xxxd oeedoc o co
o oe oxxodcoxddededcxx xxdc
xoedc oodcccoxd eoeeeeocxoc xc
oeeocoeexoee eceeeeceecooeox
coeeox eoc oe
edeedodo odoeccc
ceecxcxodxxcxdocceodc
cexddeeoeecccce

View file

@ -0,0 +1,17 @@
eecdccccdcoe
edccecodocecdcccccce
oeceoccoccee eccxccxocxo
exccceeee ccxecx
ooedecxeeeoo eeoc
o ooe ce cxoo ceec
x d e e cee xexo
xxex ee eoo ex x
xccx oo occcocceccccce xexx
ecxe oxeeoxo exeeeeeeexx eoce
c cxe cecoe ccceeeeec xoco
cocedo ooxco
eoxxcxo occcooe
coecccdedxecxdcocodcc
eccoxeooeooccccc

View file

@ -0,0 +1,17 @@
eoddcddcdcoe
eocccxo xedocdxcocoe
ocdxxxccce eexeecxoeee
ooxoxcoco cxccece
oxoexdxedexo ecexce
oxxex cexxoce c edxo
cexde ccceccxo o cdx
xoe oxocexx xxx
ex xe dxoceoocxccceeeeoo xxc
xeo x oooooexedexooeodoedxoocx
exdceoeeoeo ccccccccceoxxxe
oxecxee oedoccc
eeode oe eoeocdxo
xxxdecceddddocdccexce
ceexdxeeeeedoce

View file

@ -0,0 +1,17 @@
eodddcdddcoe
ecccocoxxcxdcdexcco
ceoecxcce cedxce oco
oxoxoxodo oexxxxe
oeocoeecxeeo e exexe
xxxxo eexedoxe coxx
xox c eeexecxo ccxcxo
xoxec oedxoex cxex
xoxec oexcoxcdexcxeeecoecxox
eexeoooccc xc ooedeodoooxocxe
eoxxoxoeeoce ccccccccoxdxeo
eecoee e oeocoe
eocexdce edcoccdc
excxoccedxdeocdcexdc
eocecceeeoeocce

View file

@ -0,0 +1,17 @@
eodddcddccee
oedocxdccxccdeocce
oooxxxcc ecxodcccoee
ooxcxcedo ecxxxxo
cdxxxceecce oeexeo
dooxoeecdxeexo odeox
e cxx eexoee ecxex
x xxx exdxoxx xcexx
eoxox ocxxcdocexcxeecceoeox
xxeooeeedc xodcoxxodddexoxe
o x oxxxdoc cccccccceeeox
o d ooe odeooxe
oeeecco eceeococ
ecxcocc dxeeoccc ddc
eodccoeeeeeocc

View file

@ -0,0 +1,17 @@
eoxdccddcoe
ocecexcccdded eco
c oxxxce eexe coeco
eexxxxdc execxoo
ecxcxxexoeo o cxxe
x dxxc ecedxe ooxxx
eecx eexexex ccexx
ecoxx dcoxcxe c xxx
ocxc oceoxeoxxddexxddcexx
xocxxddxxocxoexxeeododexex
e deocxceo cccccccceoxx
ce oeoee ocoodoc
cdoceeee oexococc
eecexcedxeeeccxcco
cxccxdxeeoedcc

View file

@ -0,0 +1,17 @@
eecedccdcoe
occoxxxcdd x cc
exdoxooc ocxoxceceo
ececoxocx c dexxe
c oxooexoe ocexxo
xcoxeeeeexec ecxxx
xcxxx cedexex xexoxo
eoxxo xceoxoco oeoxx
e exxee ocdxxococooexxx
ecodexcoxoxxdxdeexxdc
ooeexexxxocececceccoex
cocxexee oooooc
ceeceeee eoeexcoc
odcxoc ddccdodoxe
xxeccexeocode
ee

View file

@ -0,0 +1,17 @@
odecoccdo
oceoxxccd eoee
o oxxoceedo eexo
c xxodde eeoxeexco
occdeccxco coccdcxx
x xxxcexedee xcxcxxx
e xcexccxeeocooo exx
x xoeoexxxeodeex xx
coxc oxcxxxxcxxoxxe
xexxxeoxexexxxxeexxx
c eeoeexocccccxxcoex
exxxeoe oo eoxe
ecexee odedxoc
eoceexcocdddcxe
coe ceexdcoc

View file

@ -0,0 +1,17 @@
                                      
             _._:=++==+,_             
         _=,/*\+/+\=||=_ _"+_         
       ,|*|+**"^`   `"*`"~=~||+       
      ;*_\*',,_            /*|;|,     
     \^;/'^|\`\\            ".|\\,    
    ~* +`  |*/;||,           '.\||,   
   +^"-*    '\|*/"|_          ! |/|   
   ||_|`     ,//|;|*            "`|   
   |=~'`    ;||^\|".~++++++_+, =" |   
    _~;*  _;+` /* |"|___.:,,,|/,/,|   
    \^_"^ ^\,./`   `^*''* ^*"/,;_/    
     *^, ", `              ,'/*_|     
       ^\,`\+_          _=_+|_+"      
         ^*,\_!*+:;=;;.=*+_,|*        
           `*"*|~~___,_;+*"           
                                      

View file

@ -0,0 +1,17 @@
                                      
              _+***\++_               
             *'`+*+\~/_*,             
            ^_,||/~~-~+\,,            
           |__/\|;_.\,''\\,           
           / ;||"|^  /_/|/           
          |` '|*~//\   `_"|           
          \  ~*"*||~|*   |/,          
          "  ||\+/+||-_ .\||          
          "  ~\ \\|;~~+\+;||          
          |  ,|\,|_/_*___|*`          
           , "|||||""!\,"\|`          
           \`',\,*"  "",//           
            |' |||~*,:,/|/`           
             ;`**/|+;_!//'            
              *, _*\_,;*              
                                      

View file

@ -0,0 +1,17 @@
                                      
               ,****++_               
              /" ;|||\\,              
             /"__||;\*/\,             
             |__||=;,_=//             
            _".;\|+\';_||,            
            |+`||+_;;|_/||            
            ** ,||||||_|=\            
            |  ||/||\/ |"|            
            /  '|/||*/+|_|            
            ** _|/=,"/|_|^            
            '`- ||||=/|\\|            
             \_-/|_*/**;|`            
             !_ *|\\^_|;"             
              \+!*||,_/*`             
               \_ '*+_+`              
                                      

View file

@ -0,0 +1,17 @@
                                      
                +***+.                
               ,=`_/|,\               
               "  |/\+,               
               /+~||=="|              
              | '~|||./|              
              |'..*^"_|"              
              |   ~/\||\              
              |   /+\||"              
              *, ~/||+|~              
              |   /|*;*_              
              |.  |"=**/              
               ,  *|!_,|              
               / **|,*\|              
               '^/",|\|`              
                \ '~\./               
                                      

View file

@ -0,0 +1,17 @@
                                      
                 /***,                
                 |__||                
                 |`_|"                
                 |**|_                
                 *  ||                
                 ":-||                
                 ,  ||                
                 +  "|                
                /+  _~.               
                |"  +=|               
                '`.. ~`               
                 |___|                
                 |+,|_                
                 *__|=                
                 , ."=                
                                      

View file

@ -0,0 +1,17 @@
                                      
                 +***;                
                 ,/__.\               
                |_||. |               
                ||/|"^~,              
                |||\   |              
                ~*||  '|              
                |||| . *              
                ||\|`  \              
                |||~   "              
                "^//  ;/              
                \|"",.,|              
                |*~|___|              
                /!"|===`              
                |\/*__/               
                 _* '=/               
                                      

View file

@ -0,0 +1,17 @@
                                      
                 ++***~_              
                `,=||^:*,             
               //|*=\|"*|             
               |/` //,.__|            
              ||="=\||/"^|            
              \||-||//|  "            
              ||   ||||~~,|           
              ||/~|+||| '-|           
              ||+,,*|||_.:|           
              |_|;/|\~*. .|           
              |/||||_| ` ;            
              |**.^~|\-  =            
              '|\, ///` ;`            
               |^||\\.+\/             
                \^*^___/              
                   ``                 

View file

@ -0,0 +1,17 @@
                                      
                _=+"**+~_             
               /^||*||\|=\            
              |//"\/=\|| '\           
             /// ;' \|\||**|          
             ||;_ =||*/|`\          
            |||*|  /|= !| ~.|         
            \\|  ,||||/*", |         
            |/; |`||/|||"; `|         
            \\|~|+~/^||"*+  /         
            *"__,==\*|._| ,_|         
            |||+""/*\|;";.~|`         
             ||* |   `//,  /          
             \|*  |  /,/_,|           
              \|~"_*~//+_|            
               ':._=:__;*             
                                      

View file

@ -0,0 +1,17 @@
                                      
                ,=+++;;~,_            
             _;**|~~*=*|,"^,          
            ,*\/_==`+,"|||_"\         
           /|/_/|"   |;\~||=\        
           |/_ ~     "/\=\//  ,       
          `=*,/`   ,:/| /,=/|./       
          *!;/|   ,//|_ *"||/=|       
          -"=|!   !//||/ ,||=;*       
          ,/*/\==+~\_|\^:\||| |       
           |"_;__|/*\/||\!\+'+\       
          \\\/"""****\_|*//\ \'       
           \||_*       `/||` ;        
            _\\!*\_   ,',/^_/         
             , ~*+=\+`_;*:|'          
              `+;/_,+~*_+*            
                   `                  

Some files were not shown because too many files have changed in this diff Show more