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:
parent
c04a0a7454
commit
db89b73a9c
1109 changed files with 134253 additions and 17 deletions
1
.github/blob-size-allowlist.txt
vendored
1
.github/blob-size-allowlist.txt
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ratatui’s 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
95
codex-rs/Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
911
codex-rs/app-server-client/src/remote.rs
Normal file
911
codex-rs/app-server-client/src/remote.rs
Normal 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(¬ification.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}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
45
codex-rs/tui/src/app_server_tui_dispatch.rs
Normal file
45
codex-rs/tui/src/app_server_tui_dispatch.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
23
codex-rs/tui_app_server/BUILD.bazel
Normal file
23
codex-rs/tui_app_server/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
||||
149
codex-rs/tui_app_server/Cargo.toml
Normal file
149
codex-rs/tui_app_server/Cargo.toml
Normal 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 }
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_1.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_1.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▒▓▒██▒▒██▒
|
||||
▒▒█▓█▒█▓█▒▒░░▒▒ ▒ █▒
|
||||
█░█░███ ▒░ ░ █░ ░▒░░░█
|
||||
▓█▒▒████▒ ▓█░▓░█
|
||||
▒▒▓▓█▒░▒░▒▒ ▓░▒▒█
|
||||
░█ █░ ░█▓▓░░█ █▓▒░░█
|
||||
█▒ ▓█ █▒░█▓ ░▒ ░▓░
|
||||
░░▒░░ █▓▓░▓░█ ░░
|
||||
░▒░█░ ▓░░▒▒░ ▓░██████▒██ ▒ ░
|
||||
▒░▓█ ▒▓█░ ▓█ ░ ░▒▒▒▓▓███░▓█▓█░
|
||||
▒▒▒ ▒ ▒▒█▓▓░ ░▒████ ▒█ ▓█▓▒▓
|
||||
█▒█ █ ░ ██▓█▒░
|
||||
▒▒█░▒█▒ ▒▒▒█░▒█
|
||||
▒██▒▒ ██▓▓▒▓▓▓▒██▒█░█
|
||||
░█ █░░░▒▒▒█▒▓██
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_10.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_10.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒████▒██▒
|
||||
██░███▒░▓▒██
|
||||
▒▒█░░▓░░▓░█▒██
|
||||
░▒▒▓▒░▓▒▓▒███▒▒█
|
||||
▓ ▓░░ ░▒ ██▓▒▓░▓
|
||||
░░ █░█░▓▓▒ ░▒ ░
|
||||
▒ ░█ █░░░░█ ░▓█
|
||||
░░▒█▓█░░▓▒░▓▒░░
|
||||
░▒ ▒▒░▓░░█▒█▓░░
|
||||
░ █░▒█░▒▓▒█▒▒▒░█░
|
||||
█ ░░░░░ ▒█ ▒░░
|
||||
▒░██▒██ ▒░ █▓▓
|
||||
░█ ░░░░██▓█▓░▓░
|
||||
▓░██▓░█▓▒ ▓▓█
|
||||
██ ▒█▒▒█▓█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_11.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_11.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
███████▒
|
||||
▓ ▓░░░▒▒█
|
||||
▓ ▒▒░░▓▒█▓▒█
|
||||
░▒▒░░▒▓█▒▒▓▓
|
||||
▒ ▓▓▒░█▒█▓▒░░█
|
||||
░█░░░█▒▓▓░▒▓░░
|
||||
██ █░░░░░░▒░▒▒
|
||||
░ ░░▓░░▒▓ ░ ░
|
||||
▓ █░▓░░█▓█░▒░
|
||||
██ ▒░▓▒█ ▓░▒░▒
|
||||
█░▓ ░░░░▒▓░▒▒░
|
||||
▒▒▓▓░▒█▓██▓░░
|
||||
▒ █░▒▒▒▒░▓
|
||||
▒█ █░░█▒▓█░
|
||||
▒▒ ███▒█░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_12.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_12.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
█████▓
|
||||
█▒░▒▓░█▒
|
||||
░▓▒██
|
||||
▓█░░░▒▒ ░
|
||||
░ █░░░░▓▓░
|
||||
░█▓▓█▒ ▒░
|
||||
░ ░▓▒░░▒
|
||||
░ ▓█▒░░
|
||||
██ ░▓░░█░░
|
||||
░ ▓░█▓█▒
|
||||
░▓ ░ ▒██▓
|
||||
█ █░ ▒█░
|
||||
▓ ██░██▒░
|
||||
█▒▓ █░▒░░
|
||||
▒ █░▒▓▓
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_13.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_13.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▓████
|
||||
░▒▒░░
|
||||
░░▒░
|
||||
░██░▒
|
||||
█ ░░
|
||||
▓▓░░
|
||||
█ ░░
|
||||
█ ░
|
||||
▓█ ▒░▓
|
||||
░ █▒░
|
||||
█░▓▓ ░░
|
||||
░▒▒▒░
|
||||
░██░▒
|
||||
█▒▒░▒
|
||||
█ ▓ ▒
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_14.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_14.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
████▓
|
||||
█▓▒▒▓▒
|
||||
░▒░░▓ ░
|
||||
░░▓░ ▒░█
|
||||
░░░▒ ░
|
||||
░█░░ █░
|
||||
░░░░ ▓ █
|
||||
░░▒░░ ▒
|
||||
░░░░
|
||||
▒▓▓ ▓▓
|
||||
▒░ █▓█░
|
||||
░█░░▒▒▒░
|
||||
▓ ░▒▒▒░
|
||||
░▒▓█▒▒▓
|
||||
▒█ █▒▓
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_15.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_15.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
█████░▒
|
||||
░█▒░░▒▓██
|
||||
▓▓░█▒▒░ █░
|
||||
░▓░ ▓▓█▓▒▒░
|
||||
░░▒ ▒▒░░▓ ▒░
|
||||
▒░░▓░░▓▓░
|
||||
░░ ░░░░░░█░
|
||||
░░▓░░█░░░ █▓░
|
||||
░░████░░░▒▓▓░
|
||||
░▒░▓▓░▒░█▓ ▓░
|
||||
░▓░░░░▒░ ░ ▓
|
||||
░██▓▒░░▒▓ ▒
|
||||
█░▒█ ▓▓▓░ ▓░
|
||||
░▒░░▒▒▓█▒▓
|
||||
▒▒█▒▒▒▒▓
|
||||
░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_16.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_16.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒█ ███░▒
|
||||
▓▒░░█░░▒░▒▒
|
||||
░▓▓ ▒▓▒▒░░ █▒
|
||||
▓▓▓ ▓█▒▒░▒░░██░
|
||||
░░▓▒▓██▒░░█▓░░▒
|
||||
░░░█░█ ░▒▒ ░ ░▓░
|
||||
▒▒░ ▓░█░░░░▓█ █ ░
|
||||
░▓▓ ░░░░▓░░░ ▓ ░░
|
||||
▒▒░░░█░▓▒░░ ██ ▓
|
||||
█ ▒▒█▒▒▒█░▓▒░ █▒░
|
||||
░░░█ ▓█▒░▓ ▓▓░░░
|
||||
░░█ ░░ ░▓▓█ ▓
|
||||
▒░█ ░ ▓█▓▒█░
|
||||
▒░░ ▒█░▓▓█▒░
|
||||
█▓▓▒▒▓▒▒▓█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_17.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_17.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
█▒███▓▓░█▒
|
||||
▒▓██░░░█▒█░█ ▒█
|
||||
██▒▓▒▒▒░██ ░░░▒ ▒
|
||||
▓░▓▒▓░ ▒░ █░▓▒░░░▒▒
|
||||
░▓▒ ░ ░ ▓▒▒▒▓▓ █
|
||||
░▒██▓░ █▓▓░ ▓█▒▓░▓▓
|
||||
█ ▓▓░ █▓▓░▒ █ ░░▓▒░
|
||||
▓ ▒░ ▓▓░░▓░█░░▒▓█
|
||||
█▓█▓▒▒▒█░▒▒░▒▒▓▒░░░ ░
|
||||
░ ▒▓▒▒░▓█▒▓░░▒ ▒███▒
|
||||
▒▒▒▓ ████▒▒░█▓▓▒ ▒█
|
||||
▒░░▒█ ░▓░░░ ▓
|
||||
▒▒▒ █▒▒ ███▓▒▒▓
|
||||
█ ░██▒▒█░▒▓█▓░█
|
||||
░█▓▓▒██░█▒██
|
||||
░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_18.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_18.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒▒█▒▒█▓░█▒
|
||||
▒█ ▒▓███░▒▒█ █▓▓▒
|
||||
▒▓▓░█ █▒ █ ▓▒ █▓▓▒ █
|
||||
█░░█▓█▒ █ █▒░▒▓▒░▒▓▒▒▒█
|
||||
▒▒▓▓ ▓░ ▒ █▒▒▓░▓░▒▒▓▒▒▒
|
||||
▓▒░ ██░▓▒▒▒▓███░█▓▓▒▓░▓░
|
||||
░░▒▓▓ █▓█▓░ ▒▓ █░▒░▒█
|
||||
▒▓░░ ▒▒ ░░▓▒ ░▓░
|
||||
▒ █▒▒▒▓▒▓█░░█░█▓▒█ ░█░░
|
||||
▒▒▒░█▒█ ░░▓▒▒▒▒░░░▒▓░░▒ █
|
||||
░▓░▒░ █████░ ▒▒▒▓░▓█▓░▓░
|
||||
▒▒ █▒█ ░░█ ▓█▒█
|
||||
▒▒██▒▒▓ ▒█▒▒▓▒█░
|
||||
█░▓████▒▒▒▒██▒▓▒██
|
||||
░░▒▓▒▒█▓█ ▓█
|
||||
░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_19.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_19.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒▒▒█░█▒▒░▓▒
|
||||
▒█░░░▒▓▒▒▒▒█▒█░███
|
||||
██▓▓▓ ░██░ ░█▓█░█▓▒
|
||||
▓▓░██▒░ ▒▒▒██▒░██
|
||||
░░▓░▓░ █░▒ ▓ ░▒ ░▒█
|
||||
░▒▓██ ▒░█░▓ ▓▓ █▓█░
|
||||
▒▒░░█ ▓█▒▓░██░ ▓▓▓█░
|
||||
░░░░ ░▓ ▒░ █ ░ ░░░
|
||||
░█░▒█▒▓▓▒▒▒░░░░██▓█░▓ ▒ ░░
|
||||
▒▓▓█░▒█▓▒██▒█░█ ▒▒ ▓▒▒▒█▓▓░▒
|
||||
█▒ ▓█░ ██ ▒▒▒▓░▓▓ ▓▓█
|
||||
▒▒▒█▒▒ ░▓▓▒▓▓█
|
||||
█ ▒▒░░██ █▓▒▓▓░▓░
|
||||
█ ▓░█▓░█▒▒▒▓▓█ ▓█░█
|
||||
░▓▒▓▓█▒█▓▒█▓▒
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_2.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_2.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▒▓▒█▒▒▒██▒
|
||||
▒██▓█▓█░░░▒░░▒▒█░██▒
|
||||
█░█░▒██░█░░ ░ █▒█▓░░▓░█
|
||||
▒░▓▒▓████▒ ▓█▒░▓░█
|
||||
█▒ ▓█▒░▒▒▒▒▒ ▒█░▒░█
|
||||
█▓█ ░ ░█▒█▓▒█ ▒▒░█░
|
||||
█░██░ ▒▓░▓░▒░█ ▓ ░ ░
|
||||
░ ▒░ █░█░░▓█ ░█▓▓░
|
||||
█ ▒░ ▓░▒▒▒░ ▓░█████████░▒░░█
|
||||
▒▒█░ ▓░░█ ▓█ ░▒▒▒▒▒▒▓▓▒▒░█▓ ░
|
||||
▒▒▒ █ █▒▓▓░█ ░ ███████ ░██░░
|
||||
█▒▒▓▓█ ░ ██▓▓██
|
||||
▓▒▒▒░██ █▒▒█ ▒░
|
||||
░░▒▓▒▒ ██▓▓▒▓▓▓▒█░▒░░█
|
||||
░████░░▒▒▒▒░▓▓█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_20.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_20.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒█▒░░▒█▒█▒▒
|
||||
█▓▒ ▓█▒█▒▒▒░░▒▒█▒██
|
||||
██ ▒██ ░█ ░ ▒ ▒██░█▒
|
||||
▒░ ▒█░█ ▒██░▒▓█▒▒
|
||||
▒░ █░█ ▒▓ ▒░░▒█▒░░▒
|
||||
▓░█░█ ███▓░ ▓ █▒░░▒
|
||||
▓░▓█░ ██ ▓██▒ █▒░▓
|
||||
░▒▒▓░ ▓▓░ █ ░░ ░
|
||||
░▓░░▓█▒▓▒▒▒▒▒▒▒██▓▒▒▒▒█ ▓ ░▒
|
||||
█░▒░▒ ▓░░▒▒▒▒░▒ █▒▒ ░▒▒ █▓ ░░
|
||||
▒█▒▒█ █ ▒█▒░░█░ ▓▒
|
||||
█ ▒█▓█ ▒▓█▓░▓
|
||||
▒▒▒██░▒ █▓█░▓██
|
||||
▒█▓▓ ░█▒▓▓█▓ ░ ░█▓██
|
||||
░██░▒ ▒▒▒▒▒░█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_21.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_21.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒█▒█▒▒█▒██▒▒
|
||||
███░░▒▒█░▒░█▓▒░▓██▒
|
||||
▓█▒▒██▒ ░ ░▒░██▒░██
|
||||
██░▓ █ ▒█▓██▓██
|
||||
▓█▓█░ █░▓▒▒ ▒▒▒▒█
|
||||
▓ ▓░ ███▒▓▓ ▒▒▒█
|
||||
░█░░ ▒ ▓░█▓█ ▒▓▒
|
||||
░▒ ▒▓ ░█ ░ ░
|
||||
░ ░ ██▓▓▓▓▓███ ▒░█ ░█ ▓▓ ░
|
||||
░ ░▒ ░▒ ▒█░ ▒ ░█░█ ▓ ▓▓
|
||||
▓ ▓ ░░ █░ ██▒█▓ ▓░ █
|
||||
██ ▓▓▒ ▒█ ▓
|
||||
█▒ ▒▓▒ ▒▓▓██ █░
|
||||
█▒▒ █ ██▓░░▓▓▒█ ▓░
|
||||
███▓█▒▒▒▒█▒▓██░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_22.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_22.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒██▒█▒▒█▒██▒
|
||||
▒█▓█░▓▒▓░▓▒░░▓░█▓██▒
|
||||
█▓█▓░▒██░ ░ █▒███▒▒██
|
||||
▓█░██ ██░░░▒█▒
|
||||
▒░░▓█ █▒▓░▒░▓▓▓█░
|
||||
▒░█▓░ █░▓░▓▒▓░ ▒░▒▒░
|
||||
░██▒▓ ░█░▒█▓█ ░░▓░
|
||||
░░▒░░ ░▒░░▒▒ ░▒░ ░
|
||||
░░█ █ █░▒▒▓▓▓▒██▒▒█░▒ ▒█ ▒░▓
|
||||
▒░▒ █▒▒▒█ ▓█ ░▓▓░ ▒█▓▒ ░██ ▓▒▒
|
||||
▒▒▒▒░ ██ ░ ░▓██▒▓▓▓ █░
|
||||
▒█▒▒▒█ ▒██ ░██
|
||||
█ █▓ ██▒ ▒▓██ █▒▓
|
||||
█▓███ █░▓▒█▓▓▓▒█ ███
|
||||
░ ░▒▓▒▒▒▓▒▒▓▒█░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_23.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_23.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒██▒▒████▒█▒▒
|
||||
▒▒░█░▒▒█▒▒▒█░▒░█░█▒
|
||||
█ █░██▓█░ ░▓█░▒▓░░█
|
||||
▓▓░█▓▓░ ▒▓▓▒░░▓▒
|
||||
▓▓░░▓█ █▓████▓█▒░▒
|
||||
█▒░ ▓░ ▒█████▓██░░▒░█
|
||||
░░░ ░ ▓▓▓▓ ▒░░ ░██
|
||||
░▓░ ░ ░ ░█▒▒█ ░ █▓░
|
||||
▒ ▒ ░█░▓▒▒▒▒▒▓▒░▒█░▒ ▒▒ ░ ░░░
|
||||
░▒▒▒░ ▒ ▓░▒ ▒░▒▒█░ ▒▒░
|
||||
▓█░ ░ ░ █░▓▓▒░▒▓▒▓░
|
||||
█░░▒░▓ █▓░▒▒▓░
|
||||
▒ ░██▓▒▒ ▒▓ ▓█▓█▓
|
||||
▒▒▒█▓██▒░▒▒▒██ ▓▒██░
|
||||
░ █▒▒░▒▒█▒▒██░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_24.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_24.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒░▒▓███▒▒█▒
|
||||
█ ▒▓ ░▒▒░▒▒██▒██
|
||||
█ █▓▒▓█ ░ ▓░▓█░███ ▒
|
||||
██▓▓█▓░▒█▒░░▓░ ▒█▒░▒▒█
|
||||
█ ▓▓▒▓█ ░ ▓▒▒░░░▒░██
|
||||
░█▒█▒░ ███▓ ▓░▓ ▓ ▒
|
||||
░ ░░ █▓▒█▓ ▓▒▒░▒▒░▒
|
||||
░ ▒░░ ░█▒▓▒▒░░▒▓▓░░░
|
||||
░▓ ░▓▓▓▓██░░░██▒██▒░ ░ ░░
|
||||
▒ ▓ █░▓██▓▓██░▓▒▒██░ ░█░
|
||||
▒ █▒░▒█ ░ ▒█▓█▒░▒▓█░
|
||||
▒ ▒██▒ ░ ▓▓▓
|
||||
▒▓█▒░░▓ ▒▒ ▒▓▓▒█
|
||||
▓▓██▒▒ ░░▓▒▒▓░▒▒▓░
|
||||
█▓▒██▓▒▒▒▒▒██
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_25.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_25.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒█▒█▓████▒
|
||||
█ ███░▒▓▒░█░░█
|
||||
▓░▓▓██ ▓░█▒▒▒░░░▒
|
||||
░██░ ▓ ▒░ ▒░██▒▓
|
||||
█▒▒▒█▓█▒▓▓▒░ ░▓▓▒▓█
|
||||
▒█░░░▒██▓▒░▓ ▓░█░▓▓░█
|
||||
░▓░█░ ░▒▒▓▒▒▓░▒▓▒ ░▒░
|
||||
░░░▓░▓ ░▒▒▒▓░▒▒░▒░░▒
|
||||
▒█▒░ ░▒▒▒▒▒▒█░░▒▒░██░▒
|
||||
▓▓ ░▓░█░▒░░▓█▒░▒█▒▓▒░
|
||||
▒░█▓▒░░ ██▓░▒░▓░░
|
||||
░▒ ░▓█▓▒▓██▓▒▓█▓▓░▓
|
||||
▒░▒░▒▒▒█▓▓█▒▓▒░░▓
|
||||
▒▓▓▒▒▒█▒░██ █░█
|
||||
░█ █▒██▒█░█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_26.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_26.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓███ ██
|
||||
▓█░▓▓▒░█▓░█
|
||||
▓█ ░▓▒░▒ ▒█
|
||||
▓█ █░░░▒░░▒█▓▒
|
||||
░▒█▒░▓░ █▒▓▓░▒▓
|
||||
▒ ░▓▓▓ █▒▒ ▒▒▓
|
||||
░ ██▒░░▓░░▓▓ █
|
||||
▓▓ ▒░░░▒▒▒░░▓░░
|
||||
░ ▓▒█▓█░█▒▒▓▒░░
|
||||
▓▒░▓█░▒▒██▒▒█░
|
||||
░░ ▓░█ ▒█▓░█▒░░
|
||||
▒▒░░▓▒ ▓▓ ░░░
|
||||
█ █░▒ ▒░▓░▓█
|
||||
░ █▒▒ █▒██▓
|
||||
▒▓▓▒█░▒▒█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_27.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_27.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▓█████
|
||||
░▓▓▓░▓▒
|
||||
▓█░ █░▓█░
|
||||
░░░▒░░▓░░
|
||||
░ ░░▒▓█▒
|
||||
░▒▓▒ ░░░░░
|
||||
▒ ░░▒█░░
|
||||
░ ░░░░▒ ░░
|
||||
░▓ ▓ ░█░░░░
|
||||
█▒ ▓ ▒░▒█░░
|
||||
░▓ ▒▒███▓█
|
||||
░░██░░▒▓░
|
||||
░▒▒█▒█▓░▒
|
||||
▒▒▒░▒▒▓▓
|
||||
█▒ ▒▒▓
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_28.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_28.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▓██▓
|
||||
░█▒░░
|
||||
▒ ▓░░
|
||||
░▓░█░
|
||||
░ ░░
|
||||
░ ▓ ░
|
||||
▒░░ ▒░
|
||||
░▓ ░
|
||||
▓▒ ▒░
|
||||
░░▓▓░░
|
||||
░ ▒░
|
||||
░▒█▒░
|
||||
░▒█░░
|
||||
█▒▒▓░
|
||||
░ ▓█░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_29.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_29.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
██████
|
||||
█░█▓ █▒
|
||||
▒█░░ █ ░
|
||||
░░░░▒▒█▓
|
||||
▒ ░ ░ ░
|
||||
░█░░░ ▒▒
|
||||
░▒▒░░░ ▒
|
||||
░░▒░░
|
||||
░░░█░ ░
|
||||
▒░▒░░ ░
|
||||
█░░▓░▒ ▒
|
||||
░▓░░░ ▒░
|
||||
░░░░░░▒░
|
||||
░▒░█▓ ░█
|
||||
░░█ ▓█
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_3.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_3.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▒▓██▒▒▒▒█▒
|
||||
▒██▓▒░░█░ ▒▒░▓▒▒░██▒
|
||||
█▓▓█▓░█ ░ ░ ░ ███▓▒░█
|
||||
▓█▓▒░▓██▒ ░▒█ ░░▒
|
||||
█▓█░▓▒▓░░█▒▒ ▒▒▒░░▒
|
||||
▓░▒▒▓ ▓█░▒▓▒▒ ░ ▒▒░
|
||||
▒█ ░ ██▒░▒ ░█ ▓█▓░█
|
||||
█▓░█░ █▓░ ▓▒░ ░▒░▒░
|
||||
▓ █░ ▓░██░░█▓░▒██▒▒▒██▒░▒ ▓░
|
||||
█▒▓▒█ ▓▓█▓▓▓░ ░█░▒▒█ ▒▓█▓▒░░▒░░
|
||||
█▒░ ░ ░░██ ███ ███▓▓▓█▓
|
||||
██░ ▒█ ░ ▓▒█▒▓▓
|
||||
▒▒▓▓█▒█ ██▓▓ █░█
|
||||
▒▒██▒██▒▒▓▒▓█▓▒█▓░▒█
|
||||
░███▒▓░▒▒▒▒░▓▓▒
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_30.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_30.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓ ████
|
||||
▒▓▓░░▒██▒▒
|
||||
█▒░█▒▒░██▒
|
||||
░░▒░▓░▒▒░▒ ▒█
|
||||
▒█░░░▒░█░█ ░
|
||||
░█░▒█ █░░░░▓░
|
||||
▒▓░░░▒▒ ▒▓▒░ ▒░
|
||||
░ ██▒░█░ ░▓ ░
|
||||
░▒ ▒░▒░▒▓░█ ░
|
||||
░░▒░▒▒░░ ██ ░
|
||||
▒░░▓▒▒█░░░█░░
|
||||
░█▓▓█▓█▒░░ ░
|
||||
▒░▒░░▓█░░█░▓
|
||||
█▒██▒▒▓░█▓█
|
||||
▒▓▓░▒▒▒▓█
|
||||
░░░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_31.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_31.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▓████▒█
|
||||
▒██▓██▒ █▒███
|
||||
█░▒▓▓█▒▒░▓ ░▒█▒
|
||||
█░▓█▒▒█▓▒█▒▒░▒░░▒
|
||||
▒░░░░█▓█▒▒█ ▒░▓▒▒
|
||||
▓░▒░░▒░█ ▒▓██▓▓░█ ░
|
||||
▓░░ ░▒█░▒▓▒▓▓█░█░▓░
|
||||
▒▒█ ░░ ░▒ ░▒ ░░▒▓░
|
||||
░▒█▒░█▒░░░▓█░░░▒ ░
|
||||
░░░▓▓░░▒▒▒▒▒░▒░░ █
|
||||
▒█▒▓█░█ ▓███░▓░█░▒
|
||||
░░░▒▒▒█ ▒▒█ ░
|
||||
▓░█▒▒ █ ▓ ░█░▓░
|
||||
▓░▒░▓▒░░█░ █░░
|
||||
█ ▒░▒██▓▓▓█
|
||||
░░░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_32.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_32.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
█████▓▓░█▒
|
||||
▓█░██░▒░░██░░█
|
||||
▓▒█▒▒██▒▓▓░█░█▒███
|
||||
█▓▓░▒█░▓▓ ▓ █▒▒░██ █
|
||||
▓▓░█░█▒██░▓ █░█░▒▓▒█▒█
|
||||
▒▓▒▒█▒█░░▓░░█▒ ░█▓ █
|
||||
█░ ▓█░█▒░░██░█▒░▓▒▓▓░█▒
|
||||
░░░█▒ ▒░░ ▓█░▓▓▒ ▒░ ░
|
||||
▒░░▓▒ █▒░ ▒▒░███░░░▒░ ▒░
|
||||
█ ▒░░█▒█▒▒▒▒▒▒░░█░▓░▓▒
|
||||
█▒█░░▓ ░█ ███▒▓▓▓▓▓▓
|
||||
▒█░▒▒▒ █▒░▓█░
|
||||
███░░░█▒ ▒▓▒░▓ █
|
||||
▒▓▒ ░█░▓▒█░▒█ ▒▓
|
||||
░▓▒▒▒██▓█▒
|
||||
░░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_33.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_33.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒██▒█▒█▒░▓▒
|
||||
▒██░░▒█▒░▓░▓░█░█▓
|
||||
▒▓▒░████▒ ░ █▓░░█ █
|
||||
█▒▓░▓▒░█▒ █░░▒▒█
|
||||
▒▓░▓░░░▓▒▒▒ ░█▒▒▒
|
||||
▓▓█ ▒▒▒▒░▒█ ▓▒▓▒▒
|
||||
░░█ ▒██░▒░▒ ░█░░
|
||||
█░██ ███▒▓▒█ ▒ ░█
|
||||
░░░ ░ █░ ▓████▓▒▒█░░█▓▒░▒░
|
||||
▒▓░█ ▓▓█▓░░░▒▒▒▒▒░░█▒▒▒░░▓
|
||||
▒▒▒█ ░▓░▓ ▓ ███ ░░█▓▒░
|
||||
▒█▒██ █ ▓▓▓▓▒▓
|
||||
█▒ ███▓█ ▒█░█▓█▒█
|
||||
▒░ █▒█░█▓█▒ ▓█▒█░█
|
||||
▒▒██▒▒▒▒██▓▓
|
||||
░░░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_34.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_34.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒█▒████▒░█▒
|
||||
▒███▓▒▓░ ░██▒██▓█▒▒
|
||||
▒▓▓█░█ ▓░█░ ░▒▒▒█ ███
|
||||
█▓▒░█▒▓█▒ █░██▒▒
|
||||
▓▓░▒▓▓░ ░ █ ▒▒█▒▒
|
||||
█▓▒░░▓ ▒▒ ░▒█▒ ▒█▒░▒
|
||||
░█▒░▒ █▒▒█░▒▒ ░▓░▒
|
||||
▒░▒ ▓ ░█▒░▓ ░ ▓ ▒▒
|
||||
██▓▓ ▓▒▓▓ ▒▒▒██████░▒▒ ░▒░
|
||||
░░▒█▓██▒ ▓▓█░░░▒░▓▒▒▒█▓▒░░░░▒
|
||||
▓▒▒█ ░▒░█▒ ██░░░░▒ █▓█▒░█
|
||||
▓█▒▓▒▒▒ ▓▓▓░▓█
|
||||
▒█░░█▒▓█ ▒█▒ ▒▓█░
|
||||
▓▒▓░ ░██▓██▒█▒█░██▓█
|
||||
░▒▓▒▒▒▒▒▒▓▒█▒▒
|
||||
░░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_35.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_35.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒██▓▒███▒██▒
|
||||
██▒█▓░███ ░█░▓ ░█▒▒
|
||||
▒▓▓░▓██░▒█ ░ ░ █▒█▓ ░██
|
||||
█▓▓█▓█▓█▒ ██▒▒░▒
|
||||
▓▓░░▓▓▒ ▒██ ░▒█░█
|
||||
▓▓▓▓█░ █░▒ ▓▓█▒ ░▒▒░
|
||||
▒ ▓▓ ▒▒ ██▒▓ ░▒▒▒
|
||||
░░░▓ ▓▒▒▓▓█ ▓ ▓
|
||||
▓ █▒ █░░▓▓ ▓░▒▒▒▓▒▒█░░ ░░▒█
|
||||
░█▒▓█ ▓▓▓ ██▓░▓ ▒█▒▒▒▒▓ ░▓█ ░█
|
||||
▓░▒██▓▒▒░▓▒░ ░ ▒▒▒▒█▒▒█▓▓▒█░
|
||||
▓▒▒▓░ ▒▓█ █▒
|
||||
▒▓░▒▓█▓█ █▓▓▒███
|
||||
▒▒ ░█░▓▓░░█░▓▓█ ▒▓▓
|
||||
▒░▓▒▒▒▓▒▒███ ▒
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_36.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_36.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒█▒████▒██▒
|
||||
▒▒ ▒█▓▓▓█▒█▓██ ███▒
|
||||
█▒█▒███▓█ ░░ ░ █░██░██░█
|
||||
▒░ ██▒▒▒▒ ██░▒ ░
|
||||
█▓▒▓▒█░▒░▒█▓ ▒▒▓█
|
||||
▓ █▓░ █▒ ░▓█ ▒▒█
|
||||
░ ▓ ░ ▒ ▒▒ ░▒░█
|
||||
░░▒░ ▒▒ ▒▓▓ ▒░ ░
|
||||
░█ ░ ▓▓ ██ ████▒█████▒ ░▒░░
|
||||
▒█░▒ █░▒▒▓░▓ ░░▒▒▒▒▒▒▒░░ ▒▓█░
|
||||
█ █░▒ █▒█▓▒ ██▒▒▒▒▒ ░█ ▓
|
||||
██ ▒▓▓ █▓░ ▓
|
||||
▒▓░░█░█ ███ ▓█░
|
||||
██▒ ██▒▒▓░▒█░▓ ▓ █▓██
|
||||
░██▓░▒██▒██████
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_4.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_4.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▒▓█▒▒█▒██▒
|
||||
▒▓ ██░▓ ░▒▒▓█▓░ ▓██▒
|
||||
██▒░░░██ ░ ░▒░▒▒█░▓▒▒▒
|
||||
▓▓░█░ ▓██ ░██▒█▒
|
||||
▓░▓▒░▒░▒▓▒░█ ▒ ▒░█▒
|
||||
▓░░▒░ █▒░░▓█▒ █ ▒▒░█
|
||||
▒░▓░ ███▒█ ░█ █ ▓░
|
||||
░▓▒ █░▓█▒░░ ░░░
|
||||
▒░ ░▒ ▓░▓ ▒▓▓█░███▒▒▒▒██ ░░█
|
||||
░▒▓ ░ █▓▓▓█▒░░▒▒░█▓▒█▓▓▒▓░▓▓ ░
|
||||
░░▓█▒█▒▒█▒▓ ████████▒▓░░░░
|
||||
█░▒ ░▒░ █▒▓▓███
|
||||
▒▒█▓▒ █▒ ▒▓▒██▓░▓
|
||||
░░░▒▒██▒▓▓▒▓██▒██▒░█░
|
||||
█▒▒░▓░▒▒▒▒▒▓▓█░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_5.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_5.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▒▓▓█▒▒▒██▒
|
||||
▒█ █▓█▓░░█░▒█▓▒░ ██
|
||||
█▒▓▒█░█ ░ ▒▒░█▒ ███
|
||||
█░▓░▓░▓▒█ ▓▒░░░░▒
|
||||
█▒▓█▓▒▒█░▒▒█ ░ ▒░▒░▒
|
||||
░░░░▓ ▒▒░▒▓▓░▒ █▓░░
|
||||
░▓░ █ ░▒▒░▒ ░█ ██░█░█
|
||||
░▓░▒ █▒▒░▓▒░ █░▒░
|
||||
░█░▒█ ▓▒░ █░█▒▒░█░▒▒▒██▒ ░▓░
|
||||
▒▒░▒██▓██ ░ ▓▓▒▒▒█▒▓█▓░▓█░░
|
||||
▒█░░█░█▒▒▓█░ ██ █░▓░▒▓
|
||||
▒▒█▓▒▒ ░ ▓▒▓██▒
|
||||
▒▓█▒░▒█▒ ▒▒████▓█
|
||||
▒░█░███▒▓░▒▒██▒█▒░▓█
|
||||
▒▓█▒█ ▒▒▒▓▒███░
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_6.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_6.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓▒▓▓█▒▒██▒▒
|
||||
█▒▓▓█░▒██░██▓▒███▒
|
||||
███░░░█ ░ ░▓▒███▓▒▒
|
||||
▓█░█░█▒▒█ ▒█░░░░█
|
||||
█▒░░░█▒▒██▒ ▓▒▒░▒█
|
||||
▓▓▓░▓░▒█▓░▒▒░█ ▓▒▒▓░
|
||||
▒ █░░ ▒▒░▓▒▒ ▒█░▒░
|
||||
░ ░░░ ▒░▒░▓░░ ░█▒░░
|
||||
▒▓░▓░ ▓█░░█▓▓█▒░█░▒▒██▒▓▒▓░
|
||||
░░▒█▓▒▒▒▓█ ░▓▒██░░█▓▒▒▒░█░▒
|
||||
▓ ░ ▓░░░▓▓ █ ██ ░▒▒▓░
|
||||
█ ▓ ▓█░ █▓▒▓▓░░
|
||||
▓░▒▒███ ▒█▒▒▓███
|
||||
░ ░██ █ ▓░▒▒████ ▓▓█
|
||||
▒▓▓███▒▒▒░▒███
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_7.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_7.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▓░▓██▒▒██▒
|
||||
██░█▒░███▒▒▒▓ ░██
|
||||
█ █░░░█░ ░▒░░ █▓▒██
|
||||
▒▒░░░░▓█ ▒░▒█░▓█
|
||||
░█░█░░▒░▓▒█ ▓ █░░▒
|
||||
░ ▓░░ ░█▒▓░▒ █▓░░░
|
||||
░▒ ░ ▒▒░▒░▒░ ██▒░░
|
||||
▒ ▓░░ ▒█▓░█░░ █ ░░░
|
||||
▓ ░█ █ ▒▓░▒▓░░▓▓▒░░▒▓█▒░░
|
||||
░██░░▒▓░░▓█░▓▒░░▒▒█▒█▓▒░▒░
|
||||
▒ ▒▒▓█░█▒▓ ██████ ▒▓░░
|
||||
█▒ ▓▒▓▒░ █ ▓▓▓▓█
|
||||
█▓██▒▒▒▒ █▒░██▓██
|
||||
▒▒█▒░█▒▓░▒▒▒██░██▓
|
||||
░█ ░▓░▒▒█▒▓██
|
||||
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_8.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_8.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▒▒█▒▓██▒██▒
|
||||
█ █▓░░░█▒▒ ░ █
|
||||
▒░▒█░▓▓█ █ ░▓░█▒█▒█
|
||||
▒█▒█▓░██░ █ ▒▒░░▒
|
||||
█ ▓░▓█▒░▓▒ ▓█▒░░█
|
||||
░██░▒▒▒▒▒░▒█ ▒█░░░
|
||||
░█░░░ █▒▓▒░░░ ░▒░▓░█
|
||||
▒█░░▓ ░█▒▓░██▓ ▓░▓░░
|
||||
▒ ▒░░▒▒ ▓█▒░░▓█████▒░░░
|
||||
▒█▓▒▒░ █░█░░▓░▒▒▒░░▒█
|
||||
▓▓▒▒░▒░░░▓█▒█▒█ ▒█ ▓▒░
|
||||
██ ░▒░░░ ▓█▓▓▓█
|
||||
█▒▒█▒▒▒▒ ▒▓▒▒░█▓█
|
||||
▓▓█░██ ▓▓██▓▓▒█░░
|
||||
░░▒██▒░▒██▓▒░
|
||||
░░
|
||||
17
codex-rs/tui_app_server/frames/blocks/frame_9.txt
Normal file
17
codex-rs/tui_app_server/frames/blocks/frame_9.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
▓▒▒█▓██▒█
|
||||
▓█▒▓░░█ ▒ ▒▓▒▒
|
||||
▓ █░░▓█▒▒▒▓ ▒▒░█
|
||||
░░▓▓▒▒ ▒▒█░▒▒░██
|
||||
▓█ ▓▒█ ░██ █▓██▓█░░
|
||||
░ ░░░ ▒░▒▓▒▒ ░█░█░░░
|
||||
░ ░█▒░██░▒▒█ ▓█▓ ░░░
|
||||
░ ░▓▒█▒░░░▒▓▒▒▒░ ░░
|
||||
█░ ▓░ ░░░░█░░█░░░
|
||||
░▒░░░▒█░▒░▒░░░░▒▒░░░
|
||||
░▒▓▒▒░▓ ████░░ ▓▒░
|
||||
▒░░░▒█░ █▓ ▒▓░░
|
||||
▒█▒░▒▒ ▓▓▒▓░▓█
|
||||
▒▓ ▒▒░█▓█▒▓▓█░░
|
||||
█▓▒ █▒▒░▓█▓
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_1.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_1.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_10.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_10.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_11.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_11.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_12.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_12.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_13.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_13.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_14.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_14.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_15.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_15.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_16.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_16.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_17.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_17.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_18.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_18.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_19.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_19.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_2.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_2.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_20.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_20.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_21.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_21.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_22.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_22.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_23.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_23.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_24.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_24.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_25.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_25.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_26.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_26.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_27.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_27.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_28.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_28.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_29.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_29.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_3.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_3.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_30.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_30.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_31.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_31.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_32.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_32.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_33.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_33.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_34.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_34.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_35.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_35.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_36.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_36.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_4.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_4.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_5.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_5.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_6.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_6.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_7.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_7.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_8.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_8.txt
Normal 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
|
||||
17
codex-rs/tui_app_server/frames/codex/frame_9.txt
Normal file
17
codex-rs/tui_app_server/frames/codex/frame_9.txt
Normal 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
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_1.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_1.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
_._:=++==+,_
|
||||
_=,/*\+/+\=||=_ _"+_
|
||||
,|*|+**"^` `"*`"~=~||+
|
||||
;*_\*',,_ /*|;|,
|
||||
\^;/'^|\`\\ ".|\\,
|
||||
~* +` |*/;||, '.\||,
|
||||
+^"-* '\|*/"|_ ! |/|
|
||||
||_|` ,//|;|* "`|
|
||||
|=~'` ;||^\|".~++++++_+, =" |
|
||||
_~;* _;+` /* |"|___.:,,,|/,/,|
|
||||
\^_"^ ^\,./` `^*''* ^*"/,;_/
|
||||
*^, ", ` ,'/*_|
|
||||
^\,`\+_ _=_+|_+"
|
||||
^*,\_!*+:;=;;.=*+_,|*
|
||||
`*"*|~~___,_;+*"
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_10.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_10.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
_+***\++_
|
||||
*'`+*+\~/_*,
|
||||
^_,||/~~-~+\,,
|
||||
|__/\|;_.\,''\\,
|
||||
/ ;||"|^ /_/|/
|
||||
|` '|*~//\ `_"|
|
||||
\ ~*"*||~|* |/,
|
||||
" ||\+/+||-_ .\||
|
||||
" ~\ \\|;~~+\+;||
|
||||
| ,|\,|_/_*___|*`
|
||||
, "|||||""!\,"\|`
|
||||
\`',\,*" "",//
|
||||
|' |||~*,:,/|/`
|
||||
;`**/|+;_!//'
|
||||
*, _*\_,;*
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_11.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_11.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
,****++_
|
||||
/" ;|||\\,
|
||||
/"__||;\*/\,
|
||||
|__||=;,_=//
|
||||
_".;\|+\';_||,
|
||||
|+`||+_;;|_/||
|
||||
** ,||||||_|=\
|
||||
| ||/||\/ |"|
|
||||
/ '|/||*/+|_|
|
||||
** _|/=,"/|_|^
|
||||
'`- ||||=/|\\|
|
||||
\_-/|_*/**;|`
|
||||
!_ *|\\^_|;"
|
||||
\+!*||,_/*`
|
||||
\_ '*+_+`
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_12.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_12.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
+***+.
|
||||
,=`_/|,\
|
||||
" |/\+,
|
||||
/+~||=="|
|
||||
| '~|||./|
|
||||
|'..*^"_|"
|
||||
| ~/\||\
|
||||
| /+\||"
|
||||
*, ~/||+|~
|
||||
| /|*;*_
|
||||
|. |"=**/
|
||||
, *|!_,|
|
||||
/ **|,*\|
|
||||
'^/",|\|`
|
||||
\ '~\./
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_13.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_13.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
/***,
|
||||
|__||
|
||||
|`_|"
|
||||
|**|_
|
||||
* ||
|
||||
":-||
|
||||
, ||
|
||||
+ "|
|
||||
/+ _~.
|
||||
|" +=|
|
||||
'`.. ~`
|
||||
|___|
|
||||
|+,|_
|
||||
*__|=
|
||||
, ."=
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_14.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_14.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
+***;
|
||||
,/__.\
|
||||
|_||. |
|
||||
||/|"^~,
|
||||
|||\ |
|
||||
~*|| '|
|
||||
|||| . *
|
||||
||\|` \
|
||||
|||~ "
|
||||
"^// ;/
|
||||
\|"",.,|
|
||||
|*~|___|
|
||||
/!"|===`
|
||||
|\/*__/
|
||||
_* '=/
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_15.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_15.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
++***~_
|
||||
`,=||^:*,
|
||||
//|*=\|"*|
|
||||
|/` //,.__|
|
||||
||="=\||/"^|
|
||||
\||-||//| "
|
||||
|| ||||~~,|
|
||||
||/~|+||| '-|
|
||||
||+,,*|||_.:|
|
||||
|_|;/|\~*. .|
|
||||
|/||||_| ` ;
|
||||
|**.^~|\- =
|
||||
'|\, ///` ;`
|
||||
|^||\\.+\/
|
||||
\^*^___/
|
||||
``
|
||||
17
codex-rs/tui_app_server/frames/default/frame_16.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_16.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
_=+"**+~_
|
||||
/^||*||\|=\
|
||||
|//"\/=\|| '\
|
||||
/// ;' \|\||**|
|
||||
||;_ =||*/|`\
|
||||
|||*| /|= !| ~.|
|
||||
\\| ,||||/*", |
|
||||
|/; |`||/|||"; `|
|
||||
\\|~|+~/^||"*+ /
|
||||
*"__,==\*|._| ,_|
|
||||
|||+""/*\|;";.~|`
|
||||
||* | `//, /
|
||||
\|* | /,/_,|
|
||||
\|~"_*~//+_|
|
||||
':._=:__;*
|
||||
|
||||
17
codex-rs/tui_app_server/frames/default/frame_17.txt
Normal file
17
codex-rs/tui_app_server/frames/default/frame_17.txt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
,=+++;;~,_
|
||||
_;**|~~*=*|,"^,
|
||||
,*\/_==`+,"|||_"\
|
||||
/|/_/|" |;\~||=\
|
||||
|/_ ~ "/\=\// ,
|
||||
`=*,/` ,:/| /,=/|./
|
||||
*!;/| ,//|_ *"||/=|
|
||||
-"=|! !//||/ ,||=;*
|
||||
,/*/\==+~\_|\^:\||| |
|
||||
|"_;__|/*\/||\!\+'+\
|
||||
\\\/"""****\_|*//\ \'
|
||||
\||_* `/||` ;
|
||||
_\\!*\_ ,',/^_/
|
||||
, ~*+=\+`_;*:|'
|
||||
`+;/_,+~*_+*
|
||||
`
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue