mirror of
https://github.com/openai/codex.git
synced 2026-11-09 00:39:32 +00:00
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:
1 parent
c04a0a7454
commit
db89b73a9c
1109 files changed
+134243
-7
No files matched your search
@@ -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.schemas.json
|
||||||
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.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/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
|
## 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.
|
- Use concise styling helpers from ratatui’s Stylize trait.
|
||||||
- Basic spans: use "text".into()
|
- Basic spans: use "text".into()
|
||||||
- Styled spans: use "text".red(), "text".green(), "text".magenta(), "text".dim(), etc.
|
- Styled spans: use "text".red(), "text".green(), "text".magenta(), "text".dim(), etc.
|
||||||
|
|||||||
Generated
+95
@@ -1476,12 +1476,15 @@ dependencies = [
|
|||||||
"codex-core",
|
"codex-core",
|
||||||
"codex-feedback",
|
"codex-feedback",
|
||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
|
"futures",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"toml 0.9.11+spec-1.1.0",
|
"toml 0.9.11+spec-1.1.0",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1660,6 +1663,7 @@ dependencies = [
|
|||||||
"codex-state",
|
"codex-state",
|
||||||
"codex-stdio-to-uds",
|
"codex-stdio-to-uds",
|
||||||
"codex-tui",
|
"codex-tui",
|
||||||
|
"codex-tui-app-server",
|
||||||
"codex-utils-cargo-bin",
|
"codex-utils-cargo-bin",
|
||||||
"codex-utils-cli",
|
"codex-utils-cli",
|
||||||
"codex-windows-sandbox",
|
"codex-windows-sandbox",
|
||||||
@@ -2512,6 +2516,97 @@ dependencies = [
|
|||||||
"codex-protocol",
|
"codex-protocol",
|
||||||
"codex-shell-command",
|
"codex-shell-command",
|
||||||
"codex-state",
|
"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-absolute-path",
|
||||||
"codex-utils-approval-presets",
|
"codex-utils-approval-presets",
|
||||||
"codex-utils-cargo-bin",
|
"codex-utils-cargo-bin",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ members = [
|
|||||||
"stdio-to-uds",
|
"stdio-to-uds",
|
||||||
"otel",
|
"otel",
|
||||||
"tui",
|
"tui",
|
||||||
|
"tui_app_server",
|
||||||
"utils/absolute-path",
|
"utils/absolute-path",
|
||||||
"utils/cargo-bin",
|
"utils/cargo-bin",
|
||||||
"utils/git",
|
"utils/git",
|
||||||
@@ -129,6 +130,7 @@ codex-state = { path = "state" }
|
|||||||
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
codex-stdio-to-uds = { path = "stdio-to-uds" }
|
||||||
codex-test-macros = { path = "test-macros" }
|
codex-test-macros = { path = "test-macros" }
|
||||||
codex-tui = { path = "tui" }
|
codex-tui = { path = "tui" }
|
||||||
|
codex-tui-app-server = { path = "tui_app_server" }
|
||||||
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
codex-utils-absolute-path = { path = "utils/absolute-path" }
|
||||||
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
codex-utils-approval-presets = { path = "utils/approval-presets" }
|
||||||
codex-utils-cache = { path = "utils/cache" }
|
codex-utils-cache = { path = "utils/cache" }
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ codex-arg0 = { workspace = true }
|
|||||||
codex-core = { workspace = true }
|
codex-core = { workspace = true }
|
||||||
codex-feedback = { workspace = true }
|
codex-feedback = { workspace = true }
|
||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
tokio = { workspace = true, features = ["sync", "time", "rt"] }
|
||||||
|
tokio-tungstenite = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
//! bridging async `mpsc` channels on both sides. Queues are bounded so overload
|
//! bridging async `mpsc` channels on both sides. Queues are bounded so overload
|
||||||
//! surfaces as channel-full errors rather than unbounded memory growth.
|
//! surfaces as channel-full errors rather than unbounded memory growth.
|
||||||
|
|
||||||
|
mod remote;
|
||||||
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::Error as IoError;
|
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::InitializeCapabilities;
|
||||||
use codex_app_server_protocol::InitializeParams;
|
use codex_app_server_protocol::InitializeParams;
|
||||||
use codex_app_server_protocol::JSONRPCErrorError;
|
use codex_app_server_protocol::JSONRPCErrorError;
|
||||||
|
use codex_app_server_protocol::JSONRPCNotification;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_app_server_protocol::Result as JsonRpcResult;
|
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_arg0::Arg0DispatchPaths;
|
||||||
use codex_core::AuthManager;
|
use codex_core::AuthManager;
|
||||||
use codex_core::ThreadManager;
|
use codex_core::ThreadManager;
|
||||||
@@ -51,6 +56,9 @@ use tokio::time::timeout;
|
|||||||
use toml::Value as TomlValue;
|
use toml::Value as TomlValue;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub use crate::remote::RemoteAppServerClient;
|
||||||
|
pub use crate::remote::RemoteAppServerConnectArgs;
|
||||||
|
|
||||||
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
/// Raw app-server request result for typed in-process requests.
|
/// 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.
|
/// `MessageProcessor` continues to produce that shape internally.
|
||||||
pub type RequestResult = std::result::Result<JsonRpcResult, JSONRPCErrorError>;
|
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 {
|
fn event_requires_delivery(event: &InProcessServerEvent) -> bool {
|
||||||
// These terminal events drive surface shutdown/completion state. Dropping
|
// These terminal events drive surface shutdown/completion state. Dropping
|
||||||
// them under backpressure can leave exec/TUI waiting forever even though
|
// them under backpressure can leave exec/TUI waiting forever even though
|
||||||
@@ -281,6 +313,22 @@ pub struct InProcessAppServerClient {
|
|||||||
thread_manager: Arc<ThreadManager>,
|
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 {
|
impl InProcessAppServerClient {
|
||||||
/// Starts the in-process runtime and facade worker task.
|
/// Starts the in-process runtime and facade worker task.
|
||||||
///
|
///
|
||||||
@@ -457,6 +505,12 @@ impl InProcessAppServerClient {
|
|||||||
self.thread_manager.clone()
|
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.
|
/// Sends a typed client request and returns raw JSON-RPC result.
|
||||||
///
|
///
|
||||||
/// Callers that expect a concrete response type should usually prefer
|
/// 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
|
/// Extracts the JSON-RPC method name for diagnostics without extending the
|
||||||
/// protocol crate with in-process-only helpers.
|
/// 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)
|
serde_json::to_value(request)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|value| {
|
.and_then(|value| {
|
||||||
@@ -658,16 +844,29 @@ fn request_method_name(request: &ClientRequest) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||||
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
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::SessionSource as ApiSessionSource;
|
||||||
use codex_app_server_protocol::ThreadStartParams;
|
use codex_app_server_protocol::ThreadStartParams;
|
||||||
use codex_app_server_protocol::ThreadStartResponse;
|
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::AuthManager;
|
||||||
use codex_core::ThreadManager;
|
use codex_core::ThreadManager;
|
||||||
use codex_core::config::ConfigBuilder;
|
use codex_core::config::ConfigBuilder;
|
||||||
|
use futures::SinkExt;
|
||||||
|
use futures::StreamExt;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
use tokio_tungstenite::accept_async;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
async fn build_test_config() -> Config {
|
async fn build_test_config() -> Config {
|
||||||
match ConfigBuilder::default().build().await {
|
match ConfigBuilder::default().build().await {
|
||||||
@@ -705,6 +904,97 @@ mod tests {
|
|||||||
start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await
|
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]
|
#[tokio::test]
|
||||||
async fn typed_request_roundtrip_works() {
|
async fn typed_request_roundtrip_works() {
|
||||||
let client = start_test_client(SessionSource::Exec).await;
|
let client = start_test_client(SessionSource::Exec).await;
|
||||||
@@ -802,6 +1092,354 @@ mod tests {
|
|||||||
client.shutdown().await.expect("shutdown should complete");
|
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]
|
#[test]
|
||||||
fn typed_request_error_exposes_sources() {
|
fn typed_request_error_exposes_sources() {
|
||||||
let transport = TypedRequestError::Transport {
|
let transport = TypedRequestError::Transport {
|
||||||
|
|||||||
@@ -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-state = { workspace = true }
|
||||||
codex-stdio-to-uds = { workspace = true }
|
codex-stdio-to-uds = { workspace = true }
|
||||||
codex-tui = { workspace = true }
|
codex-tui = { workspace = true }
|
||||||
|
codex-tui-app-server = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
owo-colors = { workspace = true }
|
owo-colors = { workspace = true }
|
||||||
regex-lite = { workspace = true }
|
regex-lite = { workspace = true }
|
||||||
|
|||||||
+194
-4
@@ -74,6 +74,9 @@ struct MultitoolCli {
|
|||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
pub feature_toggles: FeatureToggles,
|
pub feature_toggles: FeatureToggles,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
remote: InteractiveRemoteOptions,
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
interactive: TuiCli,
|
interactive: TuiCli,
|
||||||
|
|
||||||
@@ -204,6 +207,9 @@ struct ResumeCommand {
|
|||||||
#[arg(long = "all", default_value_t = false)]
|
#[arg(long = "all", default_value_t = false)]
|
||||||
all: bool,
|
all: bool,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
remote: InteractiveRemoteOptions,
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
config_overrides: TuiCli,
|
config_overrides: TuiCli,
|
||||||
}
|
}
|
||||||
@@ -223,6 +229,9 @@ struct ForkCommand {
|
|||||||
#[arg(long = "all", default_value_t = false)]
|
#[arg(long = "all", default_value_t = false)]
|
||||||
all: bool,
|
all: bool,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
remote: InteractiveRemoteOptions,
|
||||||
|
|
||||||
#[clap(flatten)]
|
#[clap(flatten)]
|
||||||
config_overrides: TuiCli,
|
config_overrides: TuiCli,
|
||||||
}
|
}
|
||||||
@@ -494,6 +503,15 @@ struct FeatureToggles {
|
|||||||
disable: Vec<String>,
|
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 {
|
impl FeatureToggles {
|
||||||
fn to_overrides(&self) -> anyhow::Result<Vec<String>> {
|
fn to_overrides(&self) -> anyhow::Result<Vec<String>> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
@@ -561,6 +579,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
let MultitoolCli {
|
let MultitoolCli {
|
||||||
config_overrides: mut root_config_overrides,
|
config_overrides: mut root_config_overrides,
|
||||||
feature_toggles,
|
feature_toggles,
|
||||||
|
remote,
|
||||||
mut interactive,
|
mut interactive,
|
||||||
subcommand,
|
subcommand,
|
||||||
} = MultitoolCli::parse();
|
} = 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.
|
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
||||||
let toggle_overrides = feature_toggles.to_overrides()?;
|
let toggle_overrides = feature_toggles.to_overrides()?;
|
||||||
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
||||||
|
let root_remote = remote.remote;
|
||||||
|
|
||||||
match subcommand {
|
match subcommand {
|
||||||
None => {
|
None => {
|
||||||
@@ -575,10 +595,12 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
&mut interactive.config_overrides,
|
&mut interactive.config_overrides,
|
||||||
root_config_overrides.clone(),
|
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)?;
|
handle_app_exit(exit_info)?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Exec(mut exec_cli)) => {
|
Some(Subcommand::Exec(mut exec_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "exec")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut exec_cli.config_overrides,
|
&mut exec_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
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?;
|
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Review(review_args)) => {
|
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"])?;
|
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
|
||||||
exec_cli.command = Some(ExecCommand::Review(review_args));
|
exec_cli.command = Some(ExecCommand::Review(review_args));
|
||||||
prepend_config_flags(
|
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?;
|
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::McpServer) => {
|
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?;
|
codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Mcp(mut mcp_cli)) => {
|
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`).
|
// Propagate any root-level config overrides (e.g. `-c key=value`).
|
||||||
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
|
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
|
||||||
mcp_cli.run().await?;
|
mcp_cli.run().await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand {
|
Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand {
|
||||||
None => {
|
None => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app-server")?;
|
||||||
let transport = app_server_cli.listen;
|
let transport = app_server_cli.listen;
|
||||||
codex_app_server::run_main_with_transport(
|
codex_app_server::run_main_with_transport(
|
||||||
arg0_paths.clone(),
|
arg0_paths.clone(),
|
||||||
@@ -615,6 +641,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Some(AppServerSubcommand::GenerateTs(gen_cli)) => {
|
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 {
|
let options = codex_app_server_protocol::GenerateTsOptions {
|
||||||
experimental_api: gen_cli.experimental,
|
experimental_api: gen_cli.experimental,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -626,6 +656,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Some(AppServerSubcommand::GenerateJsonSchema(gen_cli)) => {
|
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(
|
codex_app_server_protocol::generate_json_with_experimental(
|
||||||
&gen_cli.out_dir,
|
&gen_cli.out_dir,
|
||||||
gen_cli.experimental,
|
gen_cli.experimental,
|
||||||
@@ -634,12 +668,14 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
},
|
},
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
Some(Subcommand::App(app_cli)) => {
|
Some(Subcommand::App(app_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "app")?;
|
||||||
app_cmd::run_app(app_cli).await?;
|
app_cmd::run_app(app_cli).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Resume(ResumeCommand {
|
Some(Subcommand::Resume(ResumeCommand {
|
||||||
session_id,
|
session_id,
|
||||||
last,
|
last,
|
||||||
all,
|
all,
|
||||||
|
remote,
|
||||||
config_overrides,
|
config_overrides,
|
||||||
})) => {
|
})) => {
|
||||||
interactive = finalize_resume_interactive(
|
interactive = finalize_resume_interactive(
|
||||||
@@ -650,13 +686,19 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
all,
|
all,
|
||||||
config_overrides,
|
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)?;
|
handle_app_exit(exit_info)?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Fork(ForkCommand {
|
Some(Subcommand::Fork(ForkCommand {
|
||||||
session_id,
|
session_id,
|
||||||
last,
|
last,
|
||||||
all,
|
all,
|
||||||
|
remote,
|
||||||
config_overrides,
|
config_overrides,
|
||||||
})) => {
|
})) => {
|
||||||
interactive = finalize_fork_interactive(
|
interactive = finalize_fork_interactive(
|
||||||
@@ -667,10 +709,16 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
all,
|
all,
|
||||||
config_overrides,
|
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)?;
|
handle_app_exit(exit_info)?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Login(mut login_cli)) => {
|
Some(Subcommand::Login(mut login_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "login")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut login_cli.config_overrides,
|
&mut login_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -702,6 +750,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Subcommand::Logout(mut logout_cli)) => {
|
Some(Subcommand::Logout(mut logout_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "logout")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut logout_cli.config_overrides,
|
&mut logout_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -709,9 +758,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
run_logout(logout_cli.config_overrides).await;
|
run_logout(logout_cli.config_overrides).await;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Completion(completion_cli)) => {
|
Some(Subcommand::Completion(completion_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "completion")?;
|
||||||
print_completion(completion_cli);
|
print_completion(completion_cli);
|
||||||
}
|
}
|
||||||
Some(Subcommand::Cloud(mut cloud_cli)) => {
|
Some(Subcommand::Cloud(mut cloud_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "cloud")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut cloud_cli.config_overrides,
|
&mut cloud_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
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 {
|
Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd {
|
||||||
SandboxCommand::Macos(mut seatbelt_cli) => {
|
SandboxCommand::Macos(mut seatbelt_cli) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox macos")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut seatbelt_cli.config_overrides,
|
&mut seatbelt_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -732,6 +784,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
SandboxCommand::Linux(mut landlock_cli) => {
|
SandboxCommand::Linux(mut landlock_cli) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox linux")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut landlock_cli.config_overrides,
|
&mut landlock_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -743,6 +796,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
SandboxCommand::Windows(mut windows_cli) => {
|
SandboxCommand::Windows(mut windows_cli) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "sandbox windows")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut windows_cli.config_overrides,
|
&mut windows_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -756,16 +810,22 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
},
|
},
|
||||||
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
|
Some(Subcommand::Debug(DebugCommand { subcommand })) => match subcommand {
|
||||||
DebugSubcommand::AppServer(cmd) => {
|
DebugSubcommand::AppServer(cmd) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "debug app-server")?;
|
||||||
run_debug_app_server_command(cmd).await?;
|
run_debug_app_server_command(cmd).await?;
|
||||||
}
|
}
|
||||||
DebugSubcommand::ClearMemories => {
|
DebugSubcommand::ClearMemories => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "debug clear-memories")?;
|
||||||
run_debug_clear_memories_command(&root_config_overrides, &interactive).await?;
|
run_debug_clear_memories_command(&root_config_overrides, &interactive).await?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(Subcommand::Execpolicy(ExecpolicyCommand { sub })) => match sub {
|
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)) => {
|
Some(Subcommand::Apply(mut apply_cli)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "apply")?;
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut apply_cli.config_overrides,
|
&mut apply_cli.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -773,16 +833,19 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
run_apply_command(apply_cli, None).await?;
|
run_apply_command(apply_cli, None).await?;
|
||||||
}
|
}
|
||||||
Some(Subcommand::ResponsesApiProxy(args)) => {
|
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))
|
tokio::task::spawn_blocking(move || codex_responses_api_proxy::run_main(args))
|
||||||
.await??;
|
.await??;
|
||||||
}
|
}
|
||||||
Some(Subcommand::StdioToUds(cmd)) => {
|
Some(Subcommand::StdioToUds(cmd)) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "stdio-to-uds")?;
|
||||||
let socket_path = cmd.socket_path;
|
let socket_path = cmd.socket_path;
|
||||||
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
|
tokio::task::spawn_blocking(move || codex_stdio_to_uds::run(socket_path.as_path()))
|
||||||
.await??;
|
.await??;
|
||||||
}
|
}
|
||||||
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
|
Some(Subcommand::Features(FeaturesCli { sub })) => match sub {
|
||||||
FeaturesSubcommand::List => {
|
FeaturesSubcommand::List => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features list")?;
|
||||||
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
// Respect root-level `-c` overrides plus top-level flags like `--profile`.
|
||||||
let mut cli_kv_overrides = root_config_overrides
|
let mut cli_kv_overrides = root_config_overrides
|
||||||
.parse_overrides()
|
.parse_overrides()
|
||||||
@@ -825,9 +888,11 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FeaturesSubcommand::Enable(FeatureSetArgs { feature }) => {
|
FeaturesSubcommand::Enable(FeatureSetArgs { feature }) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features enable")?;
|
||||||
enable_feature_in_config(&interactive, &feature).await?;
|
enable_feature_in_config(&interactive, &feature).await?;
|
||||||
}
|
}
|
||||||
FeaturesSubcommand::Disable(FeatureSetArgs { feature }) => {
|
FeaturesSubcommand::Disable(FeatureSetArgs { feature }) => {
|
||||||
|
reject_remote_mode_for_subcommand(root_remote.as_deref(), "features disable")?;
|
||||||
disable_feature_in_config(&interactive, &feature).await?;
|
disable_feature_in_config(&interactive, &feature).await?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -949,8 +1014,18 @@ fn prepend_config_flags(
|
|||||||
.splice(0..0, cli_config_overrides.raw_overrides);
|
.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(
|
async fn run_interactive_tui(
|
||||||
mut interactive: TuiCli,
|
mut interactive: TuiCli,
|
||||||
|
remote: Option<String>,
|
||||||
arg0_paths: Arg0DispatchPaths,
|
arg0_paths: Arg0DispatchPaths,
|
||||||
) -> std::io::Result<AppExitInfo> {
|
) -> std::io::Result<AppExitInfo> {
|
||||||
if let Some(prompt) = interactive.prompt.take() {
|
if let Some(prompt) = interactive.prompt.take() {
|
||||||
@@ -976,12 +1051,93 @@ async fn run_interactive_tui(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
codex_tui::run_main(
|
||||||
interactive,
|
interactive,
|
||||||
arg0_paths,
|
arg0_paths,
|
||||||
codex_core::config_loader::LoaderOverrides::default(),
|
codex_core::config_loader::LoaderOverrides::default(),
|
||||||
)
|
)
|
||||||
.await
|
.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> {
|
fn confirm(prompt: &str) -> std::io::Result<bool> {
|
||||||
@@ -1114,12 +1270,14 @@ mod tests {
|
|||||||
config_overrides: root_overrides,
|
config_overrides: root_overrides,
|
||||||
subcommand,
|
subcommand,
|
||||||
feature_toggles: _,
|
feature_toggles: _,
|
||||||
|
remote: _,
|
||||||
} = cli;
|
} = cli;
|
||||||
|
|
||||||
let Subcommand::Resume(ResumeCommand {
|
let Subcommand::Resume(ResumeCommand {
|
||||||
session_id,
|
session_id,
|
||||||
last,
|
last,
|
||||||
all,
|
all,
|
||||||
|
remote: _,
|
||||||
config_overrides: resume_cli,
|
config_overrides: resume_cli,
|
||||||
}) = subcommand.expect("resume present")
|
}) = subcommand.expect("resume present")
|
||||||
else {
|
else {
|
||||||
@@ -1143,12 +1301,14 @@ mod tests {
|
|||||||
config_overrides: root_overrides,
|
config_overrides: root_overrides,
|
||||||
subcommand,
|
subcommand,
|
||||||
feature_toggles: _,
|
feature_toggles: _,
|
||||||
|
remote: _,
|
||||||
} = cli;
|
} = cli;
|
||||||
|
|
||||||
let Subcommand::Fork(ForkCommand {
|
let Subcommand::Fork(ForkCommand {
|
||||||
session_id,
|
session_id,
|
||||||
last,
|
last,
|
||||||
all,
|
all,
|
||||||
|
remote: _,
|
||||||
config_overrides: fork_cli,
|
config_overrides: fork_cli,
|
||||||
}) = subcommand.expect("fork present")
|
}) = subcommand.expect("fork present")
|
||||||
else {
|
else {
|
||||||
@@ -1449,6 +1609,36 @@ mod tests {
|
|||||||
assert!(app_server.analytics_default_enabled);
|
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]
|
#[test]
|
||||||
fn app_server_listen_websocket_url_parses() {
|
fn app_server_listen_websocket_url_parses() {
|
||||||
let app_server = app_server_from_args(
|
let app_server = app_server_from_args(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use chrono::Duration as ChronoDuration;
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use codex_backend_client::Client as BackendClient;
|
use codex_backend_client::Client as BackendClient;
|
||||||
use codex_core::AuthManager;
|
use codex_core::AuthManager;
|
||||||
|
use codex_core::auth::AuthCredentialsStoreMode;
|
||||||
use codex_core::auth::CodexAuth;
|
use codex_core::auth::CodexAuth;
|
||||||
use codex_core::auth::RefreshTokenError;
|
use codex_core::auth::RefreshTokenError;
|
||||||
use codex_core::config_loader::CloudRequirementsLoadError;
|
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(
|
fn parse_cloud_requirements(
|
||||||
contents: &str,
|
contents: &str,
|
||||||
) -> Result<Option<ConfigRequirementsToml>, toml::de::Error> {
|
) -> Result<Option<ConfigRequirementsToml>, toml::de::Error> {
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
load("//:defs.bzl", "codex_rust_crate")
|
load("//:defs.bzl", "codex_rust_crate")
|
||||||
|
|
||||||
|
exports_files(
|
||||||
|
[
|
||||||
|
"templates/collaboration_mode/default.md",
|
||||||
|
"templates/collaboration_mode/plan.md",
|
||||||
|
],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "model_availability_nux_fixtures",
|
name = "model_availability_nux_fixtures",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
|||||||
@@ -488,6 +488,9 @@
|
|||||||
"tool_suggest": {
|
"tool_suggest": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"tui_app_server": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"undo": {
|
"undo": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@@ -2037,6 +2040,9 @@
|
|||||||
"tool_suggest": {
|
"tool_suggest": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"tui_app_server": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"undo": {
|
"undo": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ pub enum Feature {
|
|||||||
VoiceTranscription,
|
VoiceTranscription,
|
||||||
/// Enable experimental realtime voice conversation mode in the TUI.
|
/// Enable experimental realtime voice conversation mode in the TUI.
|
||||||
RealtimeConversation,
|
RealtimeConversation,
|
||||||
|
/// Route interactive startup to the app-server-backed TUI implementation.
|
||||||
|
TuiAppServer,
|
||||||
/// Prevent idle system sleep while a turn is actively running.
|
/// Prevent idle system sleep while a turn is actively running.
|
||||||
PreventIdleSleep,
|
PreventIdleSleep,
|
||||||
/// Use the Responses API WebSocket transport for OpenAI by default.
|
/// Use the Responses API WebSocket transport for OpenAI by default.
|
||||||
@@ -827,6 +829,16 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||||||
stage: Stage::UnderDevelopment,
|
stage: Stage::UnderDevelopment,
|
||||||
default_enabled: false,
|
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 {
|
FeatureSpec {
|
||||||
id: Feature::PreventIdleSleep,
|
id: Feature::PreventIdleSleep,
|
||||||
key: "prevent_idle_sleep",
|
key: "prevent_idle_sleep",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ codex-otel = { workspace = true }
|
|||||||
codex-protocol = { workspace = true }
|
codex-protocol = { workspace = true }
|
||||||
codex-shell-command = { workspace = true }
|
codex-shell-command = { workspace = true }
|
||||||
codex-state = { workspace = true }
|
codex-state = { workspace = true }
|
||||||
|
codex-tui-app-server = { workspace = true }
|
||||||
codex-utils-approval-presets = { workspace = true }
|
codex-utils-approval-presets = { workspace = true }
|
||||||
codex-utils-absolute-path = { workspace = true }
|
codex-utils-absolute-path = { workspace = true }
|
||||||
codex-utils-cli = { workspace = true }
|
codex-utils-cli = { workspace = true }
|
||||||
|
|||||||
@@ -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_backtrack;
|
||||||
mod app_event;
|
mod app_event;
|
||||||
mod app_event_sender;
|
mod app_event_sender;
|
||||||
|
mod app_server_tui_dispatch;
|
||||||
mod ascii_animation;
|
mod ascii_animation;
|
||||||
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
|
#[cfg(all(not(target_os = "linux"), feature = "voice-input"))]
|
||||||
mod audio_device;
|
mod audio_device;
|
||||||
@@ -229,6 +230,7 @@ pub mod test_backend;
|
|||||||
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||||
use crate::onboarding::onboarding_screen::run_onboarding_app;
|
use crate::onboarding::onboarding_screen::run_onboarding_app;
|
||||||
use crate::tui::Tui;
|
use crate::tui::Tui;
|
||||||
|
pub use app_server_tui_dispatch::should_use_app_server_tui;
|
||||||
pub use cli::Cli;
|
pub use cli::Cli;
|
||||||
use codex_arg0::Arg0DispatchPaths;
|
use codex_arg0::Arg0DispatchPaths;
|
||||||
pub use markdown_render::render_markdown_text;
|
pub use markdown_render::render_markdown_text;
|
||||||
@@ -1288,6 +1290,7 @@ mod tests {
|
|||||||
use codex_protocol::protocol::SessionMetaLine;
|
use codex_protocol::protocol::SessionMetaLine;
|
||||||
use codex_protocol::protocol::SessionSource;
|
use codex_protocol::protocol::SessionSource;
|
||||||
use codex_protocol::protocol::TurnContextItem;
|
use codex_protocol::protocol::TurnContextItem;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use codex_arg0::Arg0DispatchPaths;
|
use codex_arg0::Arg0DispatchPaths;
|
||||||
use codex_arg0::arg0_dispatch_or_else;
|
use codex_arg0::arg0_dispatch_or_else;
|
||||||
|
use codex_tui::AppExitInfo;
|
||||||
use codex_tui::Cli;
|
use codex_tui::Cli;
|
||||||
|
use codex_tui::ExitReason;
|
||||||
use codex_tui::run_main;
|
use codex_tui::run_main;
|
||||||
|
use codex_tui::update_action::UpdateAction;
|
||||||
use codex_utils_cli::CliConfigOverrides;
|
use codex_utils_cli::CliConfigOverrides;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -14,6 +17,65 @@ struct TopCli {
|
|||||||
inner: Cli,
|
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<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
||||||
let top_cli = TopCli::parse();
|
let top_cli = TopCli::parse();
|
||||||
@@ -22,12 +84,25 @@ fn main() -> anyhow::Result<()> {
|
|||||||
.config_overrides
|
.config_overrides
|
||||||
.raw_overrides
|
.raw_overrides
|
||||||
.splice(0..0, top_cli.config_overrides.raw_overrides);
|
.splice(0..0, top_cli.config_overrides.raw_overrides);
|
||||||
let exit_info = run_main(
|
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,
|
inner,
|
||||||
arg0_paths,
|
arg0_paths,
|
||||||
codex_core::config_loader::LoaderOverrides::default(),
|
codex_core::config_loader::LoaderOverrides::default(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?
|
||||||
|
};
|
||||||
let token_usage = exit_info.token_usage;
|
let token_usage = exit_info.token_usage;
|
||||||
if !token_usage.is_zero() {
|
if !token_usage.is_zero() {
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -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 }
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▒▓▒██▒▒██▒
|
||||||
|
▒▒█▓█▒█▓█▒▒░░▒▒ ▒ █▒
|
||||||
|
█░█░███ ▒░ ░ █░ ░▒░░░█
|
||||||
|
▓█▒▒████▒ ▓█░▓░█
|
||||||
|
▒▒▓▓█▒░▒░▒▒ ▓░▒▒█
|
||||||
|
░█ █░ ░█▓▓░░█ █▓▒░░█
|
||||||
|
█▒ ▓█ █▒░█▓ ░▒ ░▓░
|
||||||
|
░░▒░░ █▓▓░▓░█ ░░
|
||||||
|
░▒░█░ ▓░░▒▒░ ▓░██████▒██ ▒ ░
|
||||||
|
▒░▓█ ▒▓█░ ▓█ ░ ░▒▒▒▓▓███░▓█▓█░
|
||||||
|
▒▒▒ ▒ ▒▒█▓▓░ ░▒████ ▒█ ▓█▓▒▓
|
||||||
|
█▒█ █ ░ ██▓█▒░
|
||||||
|
▒▒█░▒█▒ ▒▒▒█░▒█
|
||||||
|
▒██▒▒ ██▓▓▒▓▓▓▒██▒█░█
|
||||||
|
░█ █░░░▒▒▒█▒▓██
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒████▒██▒
|
||||||
|
██░███▒░▓▒██
|
||||||
|
▒▒█░░▓░░▓░█▒██
|
||||||
|
░▒▒▓▒░▓▒▓▒███▒▒█
|
||||||
|
▓ ▓░░ ░▒ ██▓▒▓░▓
|
||||||
|
░░ █░█░▓▓▒ ░▒ ░
|
||||||
|
▒ ░█ █░░░░█ ░▓█
|
||||||
|
░░▒█▓█░░▓▒░▓▒░░
|
||||||
|
░▒ ▒▒░▓░░█▒█▓░░
|
||||||
|
░ █░▒█░▒▓▒█▒▒▒░█░
|
||||||
|
█ ░░░░░ ▒█ ▒░░
|
||||||
|
▒░██▒██ ▒░ █▓▓
|
||||||
|
░█ ░░░░██▓█▓░▓░
|
||||||
|
▓░██▓░█▓▒ ▓▓█
|
||||||
|
██ ▒█▒▒█▓█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
███████▒
|
||||||
|
▓ ▓░░░▒▒█
|
||||||
|
▓ ▒▒░░▓▒█▓▒█
|
||||||
|
░▒▒░░▒▓█▒▒▓▓
|
||||||
|
▒ ▓▓▒░█▒█▓▒░░█
|
||||||
|
░█░░░█▒▓▓░▒▓░░
|
||||||
|
██ █░░░░░░▒░▒▒
|
||||||
|
░ ░░▓░░▒▓ ░ ░
|
||||||
|
▓ █░▓░░█▓█░▒░
|
||||||
|
██ ▒░▓▒█ ▓░▒░▒
|
||||||
|
█░▓ ░░░░▒▓░▒▒░
|
||||||
|
▒▒▓▓░▒█▓██▓░░
|
||||||
|
▒ █░▒▒▒▒░▓
|
||||||
|
▒█ █░░█▒▓█░
|
||||||
|
▒▒ ███▒█░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
█████▓
|
||||||
|
█▒░▒▓░█▒
|
||||||
|
░▓▒██
|
||||||
|
▓█░░░▒▒ ░
|
||||||
|
░ █░░░░▓▓░
|
||||||
|
░█▓▓█▒ ▒░
|
||||||
|
░ ░▓▒░░▒
|
||||||
|
░ ▓█▒░░
|
||||||
|
██ ░▓░░█░░
|
||||||
|
░ ▓░█▓█▒
|
||||||
|
░▓ ░ ▒██▓
|
||||||
|
█ █░ ▒█░
|
||||||
|
▓ ██░██▒░
|
||||||
|
█▒▓ █░▒░░
|
||||||
|
▒ █░▒▓▓
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▓████
|
||||||
|
░▒▒░░
|
||||||
|
░░▒░
|
||||||
|
░██░▒
|
||||||
|
█ ░░
|
||||||
|
▓▓░░
|
||||||
|
█ ░░
|
||||||
|
█ ░
|
||||||
|
▓█ ▒░▓
|
||||||
|
░ █▒░
|
||||||
|
█░▓▓ ░░
|
||||||
|
░▒▒▒░
|
||||||
|
░██░▒
|
||||||
|
█▒▒░▒
|
||||||
|
█ ▓ ▒
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
████▓
|
||||||
|
█▓▒▒▓▒
|
||||||
|
░▒░░▓ ░
|
||||||
|
░░▓░ ▒░█
|
||||||
|
░░░▒ ░
|
||||||
|
░█░░ █░
|
||||||
|
░░░░ ▓ █
|
||||||
|
░░▒░░ ▒
|
||||||
|
░░░░
|
||||||
|
▒▓▓ ▓▓
|
||||||
|
▒░ █▓█░
|
||||||
|
░█░░▒▒▒░
|
||||||
|
▓ ░▒▒▒░
|
||||||
|
░▒▓█▒▒▓
|
||||||
|
▒█ █▒▓
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
█████░▒
|
||||||
|
░█▒░░▒▓██
|
||||||
|
▓▓░█▒▒░ █░
|
||||||
|
░▓░ ▓▓█▓▒▒░
|
||||||
|
░░▒ ▒▒░░▓ ▒░
|
||||||
|
▒░░▓░░▓▓░
|
||||||
|
░░ ░░░░░░█░
|
||||||
|
░░▓░░█░░░ █▓░
|
||||||
|
░░████░░░▒▓▓░
|
||||||
|
░▒░▓▓░▒░█▓ ▓░
|
||||||
|
░▓░░░░▒░ ░ ▓
|
||||||
|
░██▓▒░░▒▓ ▒
|
||||||
|
█░▒█ ▓▓▓░ ▓░
|
||||||
|
░▒░░▒▒▓█▒▓
|
||||||
|
▒▒█▒▒▒▒▓
|
||||||
|
░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒█ ███░▒
|
||||||
|
▓▒░░█░░▒░▒▒
|
||||||
|
░▓▓ ▒▓▒▒░░ █▒
|
||||||
|
▓▓▓ ▓█▒▒░▒░░██░
|
||||||
|
░░▓▒▓██▒░░█▓░░▒
|
||||||
|
░░░█░█ ░▒▒ ░ ░▓░
|
||||||
|
▒▒░ ▓░█░░░░▓█ █ ░
|
||||||
|
░▓▓ ░░░░▓░░░ ▓ ░░
|
||||||
|
▒▒░░░█░▓▒░░ ██ ▓
|
||||||
|
█ ▒▒█▒▒▒█░▓▒░ █▒░
|
||||||
|
░░░█ ▓█▒░▓ ▓▓░░░
|
||||||
|
░░█ ░░ ░▓▓█ ▓
|
||||||
|
▒░█ ░ ▓█▓▒█░
|
||||||
|
▒░░ ▒█░▓▓█▒░
|
||||||
|
█▓▓▒▒▓▒▒▓█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
█▒███▓▓░█▒
|
||||||
|
▒▓██░░░█▒█░█ ▒█
|
||||||
|
██▒▓▒▒▒░██ ░░░▒ ▒
|
||||||
|
▓░▓▒▓░ ▒░ █░▓▒░░░▒▒
|
||||||
|
░▓▒ ░ ░ ▓▒▒▒▓▓ █
|
||||||
|
░▒██▓░ █▓▓░ ▓█▒▓░▓▓
|
||||||
|
█ ▓▓░ █▓▓░▒ █ ░░▓▒░
|
||||||
|
▓ ▒░ ▓▓░░▓░█░░▒▓█
|
||||||
|
█▓█▓▒▒▒█░▒▒░▒▒▓▒░░░ ░
|
||||||
|
░ ▒▓▒▒░▓█▒▓░░▒ ▒███▒
|
||||||
|
▒▒▒▓ ████▒▒░█▓▓▒ ▒█
|
||||||
|
▒░░▒█ ░▓░░░ ▓
|
||||||
|
▒▒▒ █▒▒ ███▓▒▒▓
|
||||||
|
█ ░██▒▒█░▒▓█▓░█
|
||||||
|
░█▓▓▒██░█▒██
|
||||||
|
░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒▒█▒▒█▓░█▒
|
||||||
|
▒█ ▒▓███░▒▒█ █▓▓▒
|
||||||
|
▒▓▓░█ █▒ █ ▓▒ █▓▓▒ █
|
||||||
|
█░░█▓█▒ █ █▒░▒▓▒░▒▓▒▒▒█
|
||||||
|
▒▒▓▓ ▓░ ▒ █▒▒▓░▓░▒▒▓▒▒▒
|
||||||
|
▓▒░ ██░▓▒▒▒▓███░█▓▓▒▓░▓░
|
||||||
|
░░▒▓▓ █▓█▓░ ▒▓ █░▒░▒█
|
||||||
|
▒▓░░ ▒▒ ░░▓▒ ░▓░
|
||||||
|
▒ █▒▒▒▓▒▓█░░█░█▓▒█ ░█░░
|
||||||
|
▒▒▒░█▒█ ░░▓▒▒▒▒░░░▒▓░░▒ █
|
||||||
|
░▓░▒░ █████░ ▒▒▒▓░▓█▓░▓░
|
||||||
|
▒▒ █▒█ ░░█ ▓█▒█
|
||||||
|
▒▒██▒▒▓ ▒█▒▒▓▒█░
|
||||||
|
█░▓████▒▒▒▒██▒▓▒██
|
||||||
|
░░▒▓▒▒█▓█ ▓█
|
||||||
|
░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒▒▒█░█▒▒░▓▒
|
||||||
|
▒█░░░▒▓▒▒▒▒█▒█░███
|
||||||
|
██▓▓▓ ░██░ ░█▓█░█▓▒
|
||||||
|
▓▓░██▒░ ▒▒▒██▒░██
|
||||||
|
░░▓░▓░ █░▒ ▓ ░▒ ░▒█
|
||||||
|
░▒▓██ ▒░█░▓ ▓▓ █▓█░
|
||||||
|
▒▒░░█ ▓█▒▓░██░ ▓▓▓█░
|
||||||
|
░░░░ ░▓ ▒░ █ ░ ░░░
|
||||||
|
░█░▒█▒▓▓▒▒▒░░░░██▓█░▓ ▒ ░░
|
||||||
|
▒▓▓█░▒█▓▒██▒█░█ ▒▒ ▓▒▒▒█▓▓░▒
|
||||||
|
█▒ ▓█░ ██ ▒▒▒▓░▓▓ ▓▓█
|
||||||
|
▒▒▒█▒▒ ░▓▓▒▓▓█
|
||||||
|
█ ▒▒░░██ █▓▒▓▓░▓░
|
||||||
|
█ ▓░█▓░█▒▒▒▓▓█ ▓█░█
|
||||||
|
░▓▒▓▓█▒█▓▒█▓▒
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▒▓▒█▒▒▒██▒
|
||||||
|
▒██▓█▓█░░░▒░░▒▒█░██▒
|
||||||
|
█░█░▒██░█░░ ░ █▒█▓░░▓░█
|
||||||
|
▒░▓▒▓████▒ ▓█▒░▓░█
|
||||||
|
█▒ ▓█▒░▒▒▒▒▒ ▒█░▒░█
|
||||||
|
█▓█ ░ ░█▒█▓▒█ ▒▒░█░
|
||||||
|
█░██░ ▒▓░▓░▒░█ ▓ ░ ░
|
||||||
|
░ ▒░ █░█░░▓█ ░█▓▓░
|
||||||
|
█ ▒░ ▓░▒▒▒░ ▓░█████████░▒░░█
|
||||||
|
▒▒█░ ▓░░█ ▓█ ░▒▒▒▒▒▒▓▓▒▒░█▓ ░
|
||||||
|
▒▒▒ █ █▒▓▓░█ ░ ███████ ░██░░
|
||||||
|
█▒▒▓▓█ ░ ██▓▓██
|
||||||
|
▓▒▒▒░██ █▒▒█ ▒░
|
||||||
|
░░▒▓▒▒ ██▓▓▒▓▓▓▒█░▒░░█
|
||||||
|
░████░░▒▒▒▒░▓▓█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒█▒░░▒█▒█▒▒
|
||||||
|
█▓▒ ▓█▒█▒▒▒░░▒▒█▒██
|
||||||
|
██ ▒██ ░█ ░ ▒ ▒██░█▒
|
||||||
|
▒░ ▒█░█ ▒██░▒▓█▒▒
|
||||||
|
▒░ █░█ ▒▓ ▒░░▒█▒░░▒
|
||||||
|
▓░█░█ ███▓░ ▓ █▒░░▒
|
||||||
|
▓░▓█░ ██ ▓██▒ █▒░▓
|
||||||
|
░▒▒▓░ ▓▓░ █ ░░ ░
|
||||||
|
░▓░░▓█▒▓▒▒▒▒▒▒▒██▓▒▒▒▒█ ▓ ░▒
|
||||||
|
█░▒░▒ ▓░░▒▒▒▒░▒ █▒▒ ░▒▒ █▓ ░░
|
||||||
|
▒█▒▒█ █ ▒█▒░░█░ ▓▒
|
||||||
|
█ ▒█▓█ ▒▓█▓░▓
|
||||||
|
▒▒▒██░▒ █▓█░▓██
|
||||||
|
▒█▓▓ ░█▒▓▓█▓ ░ ░█▓██
|
||||||
|
░██░▒ ▒▒▒▒▒░█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒█▒█▒▒█▒██▒▒
|
||||||
|
███░░▒▒█░▒░█▓▒░▓██▒
|
||||||
|
▓█▒▒██▒ ░ ░▒░██▒░██
|
||||||
|
██░▓ █ ▒█▓██▓██
|
||||||
|
▓█▓█░ █░▓▒▒ ▒▒▒▒█
|
||||||
|
▓ ▓░ ███▒▓▓ ▒▒▒█
|
||||||
|
░█░░ ▒ ▓░█▓█ ▒▓▒
|
||||||
|
░▒ ▒▓ ░█ ░ ░
|
||||||
|
░ ░ ██▓▓▓▓▓███ ▒░█ ░█ ▓▓ ░
|
||||||
|
░ ░▒ ░▒ ▒█░ ▒ ░█░█ ▓ ▓▓
|
||||||
|
▓ ▓ ░░ █░ ██▒█▓ ▓░ █
|
||||||
|
██ ▓▓▒ ▒█ ▓
|
||||||
|
█▒ ▒▓▒ ▒▓▓██ █░
|
||||||
|
█▒▒ █ ██▓░░▓▓▒█ ▓░
|
||||||
|
███▓█▒▒▒▒█▒▓██░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒██▒█▒▒█▒██▒
|
||||||
|
▒█▓█░▓▒▓░▓▒░░▓░█▓██▒
|
||||||
|
█▓█▓░▒██░ ░ █▒███▒▒██
|
||||||
|
▓█░██ ██░░░▒█▒
|
||||||
|
▒░░▓█ █▒▓░▒░▓▓▓█░
|
||||||
|
▒░█▓░ █░▓░▓▒▓░ ▒░▒▒░
|
||||||
|
░██▒▓ ░█░▒█▓█ ░░▓░
|
||||||
|
░░▒░░ ░▒░░▒▒ ░▒░ ░
|
||||||
|
░░█ █ █░▒▒▓▓▓▒██▒▒█░▒ ▒█ ▒░▓
|
||||||
|
▒░▒ █▒▒▒█ ▓█ ░▓▓░ ▒█▓▒ ░██ ▓▒▒
|
||||||
|
▒▒▒▒░ ██ ░ ░▓██▒▓▓▓ █░
|
||||||
|
▒█▒▒▒█ ▒██ ░██
|
||||||
|
█ █▓ ██▒ ▒▓██ █▒▓
|
||||||
|
█▓███ █░▓▒█▓▓▓▒█ ███
|
||||||
|
░ ░▒▓▒▒▒▓▒▒▓▒█░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒██▒▒████▒█▒▒
|
||||||
|
▒▒░█░▒▒█▒▒▒█░▒░█░█▒
|
||||||
|
█ █░██▓█░ ░▓█░▒▓░░█
|
||||||
|
▓▓░█▓▓░ ▒▓▓▒░░▓▒
|
||||||
|
▓▓░░▓█ █▓████▓█▒░▒
|
||||||
|
█▒░ ▓░ ▒█████▓██░░▒░█
|
||||||
|
░░░ ░ ▓▓▓▓ ▒░░ ░██
|
||||||
|
░▓░ ░ ░ ░█▒▒█ ░ █▓░
|
||||||
|
▒ ▒ ░█░▓▒▒▒▒▒▓▒░▒█░▒ ▒▒ ░ ░░░
|
||||||
|
░▒▒▒░ ▒ ▓░▒ ▒░▒▒█░ ▒▒░
|
||||||
|
▓█░ ░ ░ █░▓▓▒░▒▓▒▓░
|
||||||
|
█░░▒░▓ █▓░▒▒▓░
|
||||||
|
▒ ░██▓▒▒ ▒▓ ▓█▓█▓
|
||||||
|
▒▒▒█▓██▒░▒▒▒██ ▓▒██░
|
||||||
|
░ █▒▒░▒▒█▒▒██░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒░▒▓███▒▒█▒
|
||||||
|
█ ▒▓ ░▒▒░▒▒██▒██
|
||||||
|
█ █▓▒▓█ ░ ▓░▓█░███ ▒
|
||||||
|
██▓▓█▓░▒█▒░░▓░ ▒█▒░▒▒█
|
||||||
|
█ ▓▓▒▓█ ░ ▓▒▒░░░▒░██
|
||||||
|
░█▒█▒░ ███▓ ▓░▓ ▓ ▒
|
||||||
|
░ ░░ █▓▒█▓ ▓▒▒░▒▒░▒
|
||||||
|
░ ▒░░ ░█▒▓▒▒░░▒▓▓░░░
|
||||||
|
░▓ ░▓▓▓▓██░░░██▒██▒░ ░ ░░
|
||||||
|
▒ ▓ █░▓██▓▓██░▓▒▒██░ ░█░
|
||||||
|
▒ █▒░▒█ ░ ▒█▓█▒░▒▓█░
|
||||||
|
▒ ▒██▒ ░ ▓▓▓
|
||||||
|
▒▓█▒░░▓ ▒▒ ▒▓▓▒█
|
||||||
|
▓▓██▒▒ ░░▓▒▒▓░▒▒▓░
|
||||||
|
█▓▒██▓▒▒▒▒▒██
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒█▒█▓████▒
|
||||||
|
█ ███░▒▓▒░█░░█
|
||||||
|
▓░▓▓██ ▓░█▒▒▒░░░▒
|
||||||
|
░██░ ▓ ▒░ ▒░██▒▓
|
||||||
|
█▒▒▒█▓█▒▓▓▒░ ░▓▓▒▓█
|
||||||
|
▒█░░░▒██▓▒░▓ ▓░█░▓▓░█
|
||||||
|
░▓░█░ ░▒▒▓▒▒▓░▒▓▒ ░▒░
|
||||||
|
░░░▓░▓ ░▒▒▒▓░▒▒░▒░░▒
|
||||||
|
▒█▒░ ░▒▒▒▒▒▒█░░▒▒░██░▒
|
||||||
|
▓▓ ░▓░█░▒░░▓█▒░▒█▒▓▒░
|
||||||
|
▒░█▓▒░░ ██▓░▒░▓░░
|
||||||
|
░▒ ░▓█▓▒▓██▓▒▓█▓▓░▓
|
||||||
|
▒░▒░▒▒▒█▓▓█▒▓▒░░▓
|
||||||
|
▒▓▓▒▒▒█▒░██ █░█
|
||||||
|
░█ █▒██▒█░█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓███ ██
|
||||||
|
▓█░▓▓▒░█▓░█
|
||||||
|
▓█ ░▓▒░▒ ▒█
|
||||||
|
▓█ █░░░▒░░▒█▓▒
|
||||||
|
░▒█▒░▓░ █▒▓▓░▒▓
|
||||||
|
▒ ░▓▓▓ █▒▒ ▒▒▓
|
||||||
|
░ ██▒░░▓░░▓▓ █
|
||||||
|
▓▓ ▒░░░▒▒▒░░▓░░
|
||||||
|
░ ▓▒█▓█░█▒▒▓▒░░
|
||||||
|
▓▒░▓█░▒▒██▒▒█░
|
||||||
|
░░ ▓░█ ▒█▓░█▒░░
|
||||||
|
▒▒░░▓▒ ▓▓ ░░░
|
||||||
|
█ █░▒ ▒░▓░▓█
|
||||||
|
░ █▒▒ █▒██▓
|
||||||
|
▒▓▓▒█░▒▒█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▓█████
|
||||||
|
░▓▓▓░▓▒
|
||||||
|
▓█░ █░▓█░
|
||||||
|
░░░▒░░▓░░
|
||||||
|
░ ░░▒▓█▒
|
||||||
|
░▒▓▒ ░░░░░
|
||||||
|
▒ ░░▒█░░
|
||||||
|
░ ░░░░▒ ░░
|
||||||
|
░▓ ▓ ░█░░░░
|
||||||
|
█▒ ▓ ▒░▒█░░
|
||||||
|
░▓ ▒▒███▓█
|
||||||
|
░░██░░▒▓░
|
||||||
|
░▒▒█▒█▓░▒
|
||||||
|
▒▒▒░▒▒▓▓
|
||||||
|
█▒ ▒▒▓
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▓██▓
|
||||||
|
░█▒░░
|
||||||
|
▒ ▓░░
|
||||||
|
░▓░█░
|
||||||
|
░ ░░
|
||||||
|
░ ▓ ░
|
||||||
|
▒░░ ▒░
|
||||||
|
░▓ ░
|
||||||
|
▓▒ ▒░
|
||||||
|
░░▓▓░░
|
||||||
|
░ ▒░
|
||||||
|
░▒█▒░
|
||||||
|
░▒█░░
|
||||||
|
█▒▒▓░
|
||||||
|
░ ▓█░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
██████
|
||||||
|
█░█▓ █▒
|
||||||
|
▒█░░ █ ░
|
||||||
|
░░░░▒▒█▓
|
||||||
|
▒ ░ ░ ░
|
||||||
|
░█░░░ ▒▒
|
||||||
|
░▒▒░░░ ▒
|
||||||
|
░░▒░░
|
||||||
|
░░░█░ ░
|
||||||
|
▒░▒░░ ░
|
||||||
|
█░░▓░▒ ▒
|
||||||
|
░▓░░░ ▒░
|
||||||
|
░░░░░░▒░
|
||||||
|
░▒░█▓ ░█
|
||||||
|
░░█ ▓█
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▒▓██▒▒▒▒█▒
|
||||||
|
▒██▓▒░░█░ ▒▒░▓▒▒░██▒
|
||||||
|
█▓▓█▓░█ ░ ░ ░ ███▓▒░█
|
||||||
|
▓█▓▒░▓██▒ ░▒█ ░░▒
|
||||||
|
█▓█░▓▒▓░░█▒▒ ▒▒▒░░▒
|
||||||
|
▓░▒▒▓ ▓█░▒▓▒▒ ░ ▒▒░
|
||||||
|
▒█ ░ ██▒░▒ ░█ ▓█▓░█
|
||||||
|
█▓░█░ █▓░ ▓▒░ ░▒░▒░
|
||||||
|
▓ █░ ▓░██░░█▓░▒██▒▒▒██▒░▒ ▓░
|
||||||
|
█▒▓▒█ ▓▓█▓▓▓░ ░█░▒▒█ ▒▓█▓▒░░▒░░
|
||||||
|
█▒░ ░ ░░██ ███ ███▓▓▓█▓
|
||||||
|
██░ ▒█ ░ ▓▒█▒▓▓
|
||||||
|
▒▒▓▓█▒█ ██▓▓ █░█
|
||||||
|
▒▒██▒██▒▒▓▒▓█▓▒█▓░▒█
|
||||||
|
░███▒▓░▒▒▒▒░▓▓▒
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓ ████
|
||||||
|
▒▓▓░░▒██▒▒
|
||||||
|
█▒░█▒▒░██▒
|
||||||
|
░░▒░▓░▒▒░▒ ▒█
|
||||||
|
▒█░░░▒░█░█ ░
|
||||||
|
░█░▒█ █░░░░▓░
|
||||||
|
▒▓░░░▒▒ ▒▓▒░ ▒░
|
||||||
|
░ ██▒░█░ ░▓ ░
|
||||||
|
░▒ ▒░▒░▒▓░█ ░
|
||||||
|
░░▒░▒▒░░ ██ ░
|
||||||
|
▒░░▓▒▒█░░░█░░
|
||||||
|
░█▓▓█▓█▒░░ ░
|
||||||
|
▒░▒░░▓█░░█░▓
|
||||||
|
█▒██▒▒▓░█▓█
|
||||||
|
▒▓▓░▒▒▒▓█
|
||||||
|
░░░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▓████▒█
|
||||||
|
▒██▓██▒ █▒███
|
||||||
|
█░▒▓▓█▒▒░▓ ░▒█▒
|
||||||
|
█░▓█▒▒█▓▒█▒▒░▒░░▒
|
||||||
|
▒░░░░█▓█▒▒█ ▒░▓▒▒
|
||||||
|
▓░▒░░▒░█ ▒▓██▓▓░█ ░
|
||||||
|
▓░░ ░▒█░▒▓▒▓▓█░█░▓░
|
||||||
|
▒▒█ ░░ ░▒ ░▒ ░░▒▓░
|
||||||
|
░▒█▒░█▒░░░▓█░░░▒ ░
|
||||||
|
░░░▓▓░░▒▒▒▒▒░▒░░ █
|
||||||
|
▒█▒▓█░█ ▓███░▓░█░▒
|
||||||
|
░░░▒▒▒█ ▒▒█ ░
|
||||||
|
▓░█▒▒ █ ▓ ░█░▓░
|
||||||
|
▓░▒░▓▒░░█░ █░░
|
||||||
|
█ ▒░▒██▓▓▓█
|
||||||
|
░░░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
█████▓▓░█▒
|
||||||
|
▓█░██░▒░░██░░█
|
||||||
|
▓▒█▒▒██▒▓▓░█░█▒███
|
||||||
|
█▓▓░▒█░▓▓ ▓ █▒▒░██ █
|
||||||
|
▓▓░█░█▒██░▓ █░█░▒▓▒█▒█
|
||||||
|
▒▓▒▒█▒█░░▓░░█▒ ░█▓ █
|
||||||
|
█░ ▓█░█▒░░██░█▒░▓▒▓▓░█▒
|
||||||
|
░░░█▒ ▒░░ ▓█░▓▓▒ ▒░ ░
|
||||||
|
▒░░▓▒ █▒░ ▒▒░███░░░▒░ ▒░
|
||||||
|
█ ▒░░█▒█▒▒▒▒▒▒░░█░▓░▓▒
|
||||||
|
█▒█░░▓ ░█ ███▒▓▓▓▓▓▓
|
||||||
|
▒█░▒▒▒ █▒░▓█░
|
||||||
|
███░░░█▒ ▒▓▒░▓ █
|
||||||
|
▒▓▒ ░█░▓▒█░▒█ ▒▓
|
||||||
|
░▓▒▒▒██▓█▒
|
||||||
|
░░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒██▒█▒█▒░▓▒
|
||||||
|
▒██░░▒█▒░▓░▓░█░█▓
|
||||||
|
▒▓▒░████▒ ░ █▓░░█ █
|
||||||
|
█▒▓░▓▒░█▒ █░░▒▒█
|
||||||
|
▒▓░▓░░░▓▒▒▒ ░█▒▒▒
|
||||||
|
▓▓█ ▒▒▒▒░▒█ ▓▒▓▒▒
|
||||||
|
░░█ ▒██░▒░▒ ░█░░
|
||||||
|
█░██ ███▒▓▒█ ▒ ░█
|
||||||
|
░░░ ░ █░ ▓████▓▒▒█░░█▓▒░▒░
|
||||||
|
▒▓░█ ▓▓█▓░░░▒▒▒▒▒░░█▒▒▒░░▓
|
||||||
|
▒▒▒█ ░▓░▓ ▓ ███ ░░█▓▒░
|
||||||
|
▒█▒██ █ ▓▓▓▓▒▓
|
||||||
|
█▒ ███▓█ ▒█░█▓█▒█
|
||||||
|
▒░ █▒█░█▓█▒ ▓█▒█░█
|
||||||
|
▒▒██▒▒▒▒██▓▓
|
||||||
|
░░░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒█▒████▒░█▒
|
||||||
|
▒███▓▒▓░ ░██▒██▓█▒▒
|
||||||
|
▒▓▓█░█ ▓░█░ ░▒▒▒█ ███
|
||||||
|
█▓▒░█▒▓█▒ █░██▒▒
|
||||||
|
▓▓░▒▓▓░ ░ █ ▒▒█▒▒
|
||||||
|
█▓▒░░▓ ▒▒ ░▒█▒ ▒█▒░▒
|
||||||
|
░█▒░▒ █▒▒█░▒▒ ░▓░▒
|
||||||
|
▒░▒ ▓ ░█▒░▓ ░ ▓ ▒▒
|
||||||
|
██▓▓ ▓▒▓▓ ▒▒▒██████░▒▒ ░▒░
|
||||||
|
░░▒█▓██▒ ▓▓█░░░▒░▓▒▒▒█▓▒░░░░▒
|
||||||
|
▓▒▒█ ░▒░█▒ ██░░░░▒ █▓█▒░█
|
||||||
|
▓█▒▓▒▒▒ ▓▓▓░▓█
|
||||||
|
▒█░░█▒▓█ ▒█▒ ▒▓█░
|
||||||
|
▓▒▓░ ░██▓██▒█▒█░██▓█
|
||||||
|
░▒▓▒▒▒▒▒▒▓▒█▒▒
|
||||||
|
░░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒██▓▒███▒██▒
|
||||||
|
██▒█▓░███ ░█░▓ ░█▒▒
|
||||||
|
▒▓▓░▓██░▒█ ░ ░ █▒█▓ ░██
|
||||||
|
█▓▓█▓█▓█▒ ██▒▒░▒
|
||||||
|
▓▓░░▓▓▒ ▒██ ░▒█░█
|
||||||
|
▓▓▓▓█░ █░▒ ▓▓█▒ ░▒▒░
|
||||||
|
▒ ▓▓ ▒▒ ██▒▓ ░▒▒▒
|
||||||
|
░░░▓ ▓▒▒▓▓█ ▓ ▓
|
||||||
|
▓ █▒ █░░▓▓ ▓░▒▒▒▓▒▒█░░ ░░▒█
|
||||||
|
░█▒▓█ ▓▓▓ ██▓░▓ ▒█▒▒▒▒▓ ░▓█ ░█
|
||||||
|
▓░▒██▓▒▒░▓▒░ ░ ▒▒▒▒█▒▒█▓▓▒█░
|
||||||
|
▓▒▒▓░ ▒▓█ █▒
|
||||||
|
▒▓░▒▓█▓█ █▓▓▒███
|
||||||
|
▒▒ ░█░▓▓░░█░▓▓█ ▒▓▓
|
||||||
|
▒░▓▒▒▒▓▒▒███ ▒
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒█▒████▒██▒
|
||||||
|
▒▒ ▒█▓▓▓█▒█▓██ ███▒
|
||||||
|
█▒█▒███▓█ ░░ ░ █░██░██░█
|
||||||
|
▒░ ██▒▒▒▒ ██░▒ ░
|
||||||
|
█▓▒▓▒█░▒░▒█▓ ▒▒▓█
|
||||||
|
▓ █▓░ █▒ ░▓█ ▒▒█
|
||||||
|
░ ▓ ░ ▒ ▒▒ ░▒░█
|
||||||
|
░░▒░ ▒▒ ▒▓▓ ▒░ ░
|
||||||
|
░█ ░ ▓▓ ██ ████▒█████▒ ░▒░░
|
||||||
|
▒█░▒ █░▒▒▓░▓ ░░▒▒▒▒▒▒▒░░ ▒▓█░
|
||||||
|
█ █░▒ █▒█▓▒ ██▒▒▒▒▒ ░█ ▓
|
||||||
|
██ ▒▓▓ █▓░ ▓
|
||||||
|
▒▓░░█░█ ███ ▓█░
|
||||||
|
██▒ ██▒▒▓░▒█░▓ ▓ █▓██
|
||||||
|
░██▓░▒██▒██████
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▒▓█▒▒█▒██▒
|
||||||
|
▒▓ ██░▓ ░▒▒▓█▓░ ▓██▒
|
||||||
|
██▒░░░██ ░ ░▒░▒▒█░▓▒▒▒
|
||||||
|
▓▓░█░ ▓██ ░██▒█▒
|
||||||
|
▓░▓▒░▒░▒▓▒░█ ▒ ▒░█▒
|
||||||
|
▓░░▒░ █▒░░▓█▒ █ ▒▒░█
|
||||||
|
▒░▓░ ███▒█ ░█ █ ▓░
|
||||||
|
░▓▒ █░▓█▒░░ ░░░
|
||||||
|
▒░ ░▒ ▓░▓ ▒▓▓█░███▒▒▒▒██ ░░█
|
||||||
|
░▒▓ ░ █▓▓▓█▒░░▒▒░█▓▒█▓▓▒▓░▓▓ ░
|
||||||
|
░░▓█▒█▒▒█▒▓ ████████▒▓░░░░
|
||||||
|
█░▒ ░▒░ █▒▓▓███
|
||||||
|
▒▒█▓▒ █▒ ▒▓▒██▓░▓
|
||||||
|
░░░▒▒██▒▓▓▒▓██▒██▒░█░
|
||||||
|
█▒▒░▓░▒▒▒▒▒▓▓█░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▒▓▓█▒▒▒██▒
|
||||||
|
▒█ █▓█▓░░█░▒█▓▒░ ██
|
||||||
|
█▒▓▒█░█ ░ ▒▒░█▒ ███
|
||||||
|
█░▓░▓░▓▒█ ▓▒░░░░▒
|
||||||
|
█▒▓█▓▒▒█░▒▒█ ░ ▒░▒░▒
|
||||||
|
░░░░▓ ▒▒░▒▓▓░▒ █▓░░
|
||||||
|
░▓░ █ ░▒▒░▒ ░█ ██░█░█
|
||||||
|
░▓░▒ █▒▒░▓▒░ █░▒░
|
||||||
|
░█░▒█ ▓▒░ █░█▒▒░█░▒▒▒██▒ ░▓░
|
||||||
|
▒▒░▒██▓██ ░ ▓▓▒▒▒█▒▓█▓░▓█░░
|
||||||
|
▒█░░█░█▒▒▓█░ ██ █░▓░▒▓
|
||||||
|
▒▒█▓▒▒ ░ ▓▒▓██▒
|
||||||
|
▒▓█▒░▒█▒ ▒▒████▓█
|
||||||
|
▒░█░███▒▓░▒▒██▒█▒░▓█
|
||||||
|
▒▓█▒█ ▒▒▒▓▒███░
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓▒▓▓█▒▒██▒▒
|
||||||
|
█▒▓▓█░▒██░██▓▒███▒
|
||||||
|
███░░░█ ░ ░▓▒███▓▒▒
|
||||||
|
▓█░█░█▒▒█ ▒█░░░░█
|
||||||
|
█▒░░░█▒▒██▒ ▓▒▒░▒█
|
||||||
|
▓▓▓░▓░▒█▓░▒▒░█ ▓▒▒▓░
|
||||||
|
▒ █░░ ▒▒░▓▒▒ ▒█░▒░
|
||||||
|
░ ░░░ ▒░▒░▓░░ ░█▒░░
|
||||||
|
▒▓░▓░ ▓█░░█▓▓█▒░█░▒▒██▒▓▒▓░
|
||||||
|
░░▒█▓▒▒▒▓█ ░▓▒██░░█▓▒▒▒░█░▒
|
||||||
|
▓ ░ ▓░░░▓▓ █ ██ ░▒▒▓░
|
||||||
|
█ ▓ ▓█░ █▓▒▓▓░░
|
||||||
|
▓░▒▒███ ▒█▒▒▓███
|
||||||
|
░ ░██ █ ▓░▒▒████ ▓▓█
|
||||||
|
▒▓▓███▒▒▒░▒███
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▓░▓██▒▒██▒
|
||||||
|
██░█▒░███▒▒▒▓ ░██
|
||||||
|
█ █░░░█░ ░▒░░ █▓▒██
|
||||||
|
▒▒░░░░▓█ ▒░▒█░▓█
|
||||||
|
░█░█░░▒░▓▒█ ▓ █░░▒
|
||||||
|
░ ▓░░ ░█▒▓░▒ █▓░░░
|
||||||
|
░▒ ░ ▒▒░▒░▒░ ██▒░░
|
||||||
|
▒ ▓░░ ▒█▓░█░░ █ ░░░
|
||||||
|
▓ ░█ █ ▒▓░▒▓░░▓▓▒░░▒▓█▒░░
|
||||||
|
░██░░▒▓░░▓█░▓▒░░▒▒█▒█▓▒░▒░
|
||||||
|
▒ ▒▒▓█░█▒▓ ██████ ▒▓░░
|
||||||
|
█▒ ▓▒▓▒░ █ ▓▓▓▓█
|
||||||
|
█▓██▒▒▒▒ █▒░██▓██
|
||||||
|
▒▒█▒░█▒▓░▒▒▒██░██▓
|
||||||
|
░█ ░▓░▒▒█▒▓██
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▒▒█▒▓██▒██▒
|
||||||
|
█ █▓░░░█▒▒ ░ █
|
||||||
|
▒░▒█░▓▓█ █ ░▓░█▒█▒█
|
||||||
|
▒█▒█▓░██░ █ ▒▒░░▒
|
||||||
|
█ ▓░▓█▒░▓▒ ▓█▒░░█
|
||||||
|
░██░▒▒▒▒▒░▒█ ▒█░░░
|
||||||
|
░█░░░ █▒▓▒░░░ ░▒░▓░█
|
||||||
|
▒█░░▓ ░█▒▓░██▓ ▓░▓░░
|
||||||
|
▒ ▒░░▒▒ ▓█▒░░▓█████▒░░░
|
||||||
|
▒█▓▒▒░ █░█░░▓░▒▒▒░░▒█
|
||||||
|
▓▓▒▒░▒░░░▓█▒█▒█ ▒█ ▓▒░
|
||||||
|
██ ░▒░░░ ▓█▓▓▓█
|
||||||
|
█▒▒█▒▒▒▒ ▒▓▒▒░█▓█
|
||||||
|
▓▓█░██ ▓▓██▓▓▒█░░
|
||||||
|
░░▒██▒░▒██▓▒░
|
||||||
|
░░
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
▓▒▒█▓██▒█
|
||||||
|
▓█▒▓░░█ ▒ ▒▓▒▒
|
||||||
|
▓ █░░▓█▒▒▒▓ ▒▒░█
|
||||||
|
░░▓▓▒▒ ▒▒█░▒▒░██
|
||||||
|
▓█ ▓▒█ ░██ █▓██▓█░░
|
||||||
|
░ ░░░ ▒░▒▓▒▒ ░█░█░░░
|
||||||
|
░ ░█▒░██░▒▒█ ▓█▓ ░░░
|
||||||
|
░ ░▓▒█▒░░░▒▓▒▒▒░ ░░
|
||||||
|
█░ ▓░ ░░░░█░░█░░░
|
||||||
|
░▒░░░▒█░▒░▒░░░░▒▒░░░
|
||||||
|
░▒▓▒▒░▓ ████░░ ▓▒░
|
||||||
|
▒░░░▒█░ █▓ ▒▓░░
|
||||||
|
▒█▒░▒▒ ▓▓▒▓░▓█
|
||||||
|
▒▓ ▒▒░█▓█▒▓▓█░░
|
||||||
|
█▓▒ █▒▒░▓█▓
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
_._:=++==+,_
|
||||||
|
_=,/*\+/+\=||=_ _"+_
|
||||||
|
,|*|+**"^` `"*`"~=~||+
|
||||||
|
;*_\*',,_ /*|;|,
|
||||||
|
\^;/'^|\`\\ ".|\\,
|
||||||
|
~* +` |*/;||, '.\||,
|
||||||
|
+^"-* '\|*/"|_ ! |/|
|
||||||
|
||_|` ,//|;|* "`|
|
||||||
|
|=~'` ;||^\|".~++++++_+, =" |
|
||||||
|
_~;* _;+` /* |"|___.:,,,|/,/,|
|
||||||
|
\^_"^ ^\,./` `^*''* ^*"/,;_/
|
||||||
|
*^, ", ` ,'/*_|
|
||||||
|
^\,`\+_ _=_+|_+"
|
||||||
|
^*,\_!*+:;=;;.=*+_,|*
|
||||||
|
`*"*|~~___,_;+*"
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
_+***\++_
|
||||||
|
*'`+*+\~/_*,
|
||||||
|
^_,||/~~-~+\,,
|
||||||
|
|__/\|;_.\,''\\,
|
||||||
|
/ ;||"|^ /_/|/
|
||||||
|
|` '|*~//\ `_"|
|
||||||
|
\ ~*"*||~|* |/,
|
||||||
|
" ||\+/+||-_ .\||
|
||||||
|
" ~\ \\|;~~+\+;||
|
||||||
|
| ,|\,|_/_*___|*`
|
||||||
|
, "|||||""!\,"\|`
|
||||||
|
\`',\,*" "",//
|
||||||
|
|' |||~*,:,/|/`
|
||||||
|
;`**/|+;_!//'
|
||||||
|
*, _*\_,;*
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
,****++_
|
||||||
|
/" ;|||\\,
|
||||||
|
/"__||;\*/\,
|
||||||
|
|__||=;,_=//
|
||||||
|
_".;\|+\';_||,
|
||||||
|
|+`||+_;;|_/||
|
||||||
|
** ,||||||_|=\
|
||||||
|
| ||/||\/ |"|
|
||||||
|
/ '|/||*/+|_|
|
||||||
|
** _|/=,"/|_|^
|
||||||
|
'`- ||||=/|\\|
|
||||||
|
\_-/|_*/**;|`
|
||||||
|
!_ *|\\^_|;"
|
||||||
|
\+!*||,_/*`
|
||||||
|
\_ '*+_+`
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
+***+.
|
||||||
|
,=`_/|,\
|
||||||
|
" |/\+,
|
||||||
|
/+~||=="|
|
||||||
|
| '~|||./|
|
||||||
|
|'..*^"_|"
|
||||||
|
| ~/\||\
|
||||||
|
| /+\||"
|
||||||
|
*, ~/||+|~
|
||||||
|
| /|*;*_
|
||||||
|
|. |"=**/
|
||||||
|
, *|!_,|
|
||||||
|
/ **|,*\|
|
||||||
|
'^/",|\|`
|
||||||
|
\ '~\./
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
/***,
|
||||||
|
|__||
|
||||||
|
|`_|"
|
||||||
|
|**|_
|
||||||
|
* ||
|
||||||
|
":-||
|
||||||
|
, ||
|
||||||
|
+ "|
|
||||||
|
/+ _~.
|
||||||
|
|" +=|
|
||||||
|
'`.. ~`
|
||||||
|
|___|
|
||||||
|
|+,|_
|
||||||
|
*__|=
|
||||||
|
, ."=
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
+***;
|
||||||
|
,/__.\
|
||||||
|
|_||. |
|
||||||
|
||/|"^~,
|
||||||
|
|||\ |
|
||||||
|
~*|| '|
|
||||||
|
|||| . *
|
||||||
|
||\|` \
|
||||||
|
|||~ "
|
||||||
|
"^// ;/
|
||||||
|
\|"",.,|
|
||||||
|
|*~|___|
|
||||||
|
/!"|===`
|
||||||
|
|\/*__/
|
||||||
|
_* '=/
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
++***~_
|
||||||
|
`,=||^:*,
|
||||||
|
//|*=\|"*|
|
||||||
|
|/` //,.__|
|
||||||
|
||="=\||/"^|
|
||||||
|
\||-||//| "
|
||||||
|
|| ||||~~,|
|
||||||
|
||/~|+||| '-|
|
||||||
|
||+,,*|||_.:|
|
||||||
|
|_|;/|\~*. .|
|
||||||
|
|/||||_| ` ;
|
||||||
|
|**.^~|\- =
|
||||||
|
'|\, ///` ;`
|
||||||
|
|^||\\.+\/
|
||||||
|
\^*^___/
|
||||||
|
``
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
_=+"**+~_
|
||||||
|
/^||*||\|=\
|
||||||
|
|//"\/=\|| '\
|
||||||
|
/// ;' \|\||**|
|
||||||
|
||;_ =||*/|`\
|
||||||
|
|||*| /|= !| ~.|
|
||||||
|
\\| ,||||/*", |
|
||||||
|
|/; |`||/|||"; `|
|
||||||
|
\\|~|+~/^||"*+ /
|
||||||
|
*"__,==\*|._| ,_|
|
||||||
|
|||+""/*\|;";.~|`
|
||||||
|
||* | `//, /
|
||||||
|
\|* | /,/_,|
|
||||||
|
\|~"_*~//+_|
|
||||||
|
':._=:__;*
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
,=+++;;~,_
|
||||||
|
_;**|~~*=*|,"^,
|
||||||
|
,*\/_==`+,"|||_"\
|
||||||
|
/|/_/|" |;\~||=\
|
||||||
|
|/_ ~ "/\=\// ,
|
||||||
|
`=*,/` ,:/| /,=/|./
|
||||||
|
*!;/| ,//|_ *"||/=|
|
||||||
|
-"=|! !//||/ ,||=;*
|
||||||
|
,/*/\==+~\_|\^:\||| |
|
||||||
|
|"_;__|/*\/||\!\+'+\
|
||||||
|
\\\/"""****\_|*//\ \'
|
||||||
|
\||_* `/||` ;
|
||||||
|
_\\!*\_ ,',/^_/
|
||||||
|
, ~*+=\+`_;*:|'
|
||||||
|
`+;/_,+~*_+*
|
||||||
|
`
|
||||||
Loaded 100 of 1109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user