mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[1 of 3] Support long raw TUI goal objectives (#27508)
## Stack 1. **[1 of 3] Support long raw TUI goal objectives** - this PR 2. [2 of 3] Support long pasted text in TUI goals - #27509 3. [3 of 3] Support images in TUI goals - #27510 ## Why `thread/goal/set` limits persisted objective text to 4000 characters. The TUI used to reject raw `/goal` objectives above that limit, even though the client can make them usable by writing the long text to a file and storing a short objective that points at that file. This also needs to work for remote app-server sessions: filesystem API calls must create files on the app-server host, and the stored path must be meaningful to the agent on that host. ## What Changed - Adds an app-server-host path helper so TUI code can build paths that are resolved on the app-server host rather than the TUI host. - Adds TUI app-server session helpers for `fs/createDirectory`, `fs/writeFile`, `fs/readFile`, and `fs/remove` that work for embedded and remote app-server sessions without changing the app-server protocol. - Materializes oversized raw `/goal` objectives into `$CODEX_HOME/attachments/<uuid>/goal-objective.md` through the app-server filesystem APIs, then stores a short, readable objective that directs the agent to that file. - Reads managed objective files back for `/goal edit`. Other goal UI renders the readable stored objective normally, without managed-file-specific presentation logic. - Recognizes managed references only when they name the expected generated file under the app server's reported `$CODEX_HOME`, and cleans up newly materialized files when goal replacement or setting does not complete. ## Verification - Added/updated TUI tests for raw oversized `/goal` submission, large inline-paste expansion, queued oversized goals, app-facing materialization before `thread/goal/set`, managed-path validation, editing, and cleanup. - Added/updated app-server-client remote coverage for initialized remote Codex home handling. ## Manual Testing - Ran the real TUI against a Unix-socket app server with different local and server `$CODEX_HOME` directories. Oversized goals wrote only under the server home, and persisted references used the server-canonical path rather than the TUI path. - Exercised 3,999-, 4,000-, and 4,001-character raw objectives. The first two stayed inline without new files; the 4,001-character objective became a managed objective file. - Submitted a larger 8,275-character objective, verified its full contents on the app-server host, and observed the goal continuation open the referenced server-side file. - Opened `/goal edit` for a managed objective and verified the full text was restored through remote `fs/readFile`. - Submitted an oversized replacement while a goal was active, verified no file was written before confirmation, then canceled and confirmed that the existing goal and attachment count were unchanged.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
use super::AppServerSession;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use codex_app_server_client::AppServerPath;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::FsCreateDirectoryParams;
|
||||
use codex_app_server_protocol::FsCreateDirectoryResponse;
|
||||
use codex_app_server_protocol::FsReadFileParams;
|
||||
use codex_app_server_protocol::FsReadFileResponse;
|
||||
use codex_app_server_protocol::FsRemoveParams;
|
||||
use codex_app_server_protocol::FsRemoveResponse;
|
||||
use codex_app_server_protocol::FsWriteFileParams;
|
||||
use codex_app_server_protocol::FsWriteFileResponse;
|
||||
use codex_app_server_protocol::JSONRPCRequest;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::json;
|
||||
|
||||
impl AppServerSession {
|
||||
pub(crate) async fn fs_create_directory_all_path(
|
||||
&mut self,
|
||||
path: &AppServerPath,
|
||||
) -> Result<()> {
|
||||
self.request_fs_path::<FsCreateDirectoryResponse>(
|
||||
"fs/createDirectory",
|
||||
path,
|
||||
|request_id, path| ClientRequest::FsCreateDirectory {
|
||||
request_id,
|
||||
params: FsCreateDirectoryParams {
|
||||
path,
|
||||
recursive: Some(true),
|
||||
},
|
||||
},
|
||||
json!({ "path": path.as_str(), "recursive": true }),
|
||||
)
|
||||
.await
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
pub(crate) async fn fs_write_file_path(
|
||||
&mut self,
|
||||
path: &AppServerPath,
|
||||
bytes: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let data_base64 = STANDARD.encode(bytes);
|
||||
self.request_fs_path::<FsWriteFileResponse>(
|
||||
"fs/writeFile",
|
||||
path,
|
||||
|request_id, path| ClientRequest::FsWriteFile {
|
||||
request_id,
|
||||
params: FsWriteFileParams {
|
||||
path,
|
||||
data_base64: data_base64.clone(),
|
||||
},
|
||||
},
|
||||
json!({ "path": path.as_str(), "dataBase64": data_base64 }),
|
||||
)
|
||||
.await
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
pub(crate) async fn fs_read_file_path(&mut self, path: &AppServerPath) -> Result<Vec<u8>> {
|
||||
let response: FsReadFileResponse = self
|
||||
.request_fs_path(
|
||||
"fs/readFile",
|
||||
path,
|
||||
|request_id, path| ClientRequest::FsReadFile {
|
||||
request_id,
|
||||
params: FsReadFileParams { path },
|
||||
},
|
||||
json!({ "path": path.as_str() }),
|
||||
)
|
||||
.await?;
|
||||
STANDARD
|
||||
.decode(response.data_base64)
|
||||
.wrap_err("fs/readFile returned invalid base64 data")
|
||||
}
|
||||
|
||||
pub(crate) async fn fs_remove_path(&mut self, path: &AppServerPath) -> Result<()> {
|
||||
self.request_fs_path::<FsRemoveResponse>(
|
||||
"fs/remove",
|
||||
path,
|
||||
|request_id, path| ClientRequest::FsRemove {
|
||||
request_id,
|
||||
params: FsRemoveParams {
|
||||
path,
|
||||
recursive: None,
|
||||
force: None,
|
||||
},
|
||||
},
|
||||
json!({ "path": path.as_str() }),
|
||||
)
|
||||
.await
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
async fn request_fs_path<T: DeserializeOwned>(
|
||||
&mut self,
|
||||
method: &str,
|
||||
path: &AppServerPath,
|
||||
local_request: impl FnOnce(RequestId, AbsolutePathBuf) -> ClientRequest,
|
||||
remote_params: serde_json::Value,
|
||||
) -> Result<T> {
|
||||
let request_id = self.next_request_id();
|
||||
match self.request_handle() {
|
||||
AppServerRequestHandle::Remote(handle) => {
|
||||
let response = handle
|
||||
.request_json_rpc(JSONRPCRequest {
|
||||
id: request_id,
|
||||
method: method.to_string(),
|
||||
params: Some(remote_params),
|
||||
trace: None,
|
||||
})
|
||||
.await
|
||||
.wrap_err_with(|| format!("{method} failed in TUI"))?;
|
||||
serde_json::from_value(response.map_err(|source| {
|
||||
color_eyre::eyre::eyre!("{method} failed in TUI: {}", source.message)
|
||||
})?)
|
||||
.wrap_err_with(|| format!("{method} returned invalid data"))
|
||||
}
|
||||
AppServerRequestHandle::InProcess(_) => {
|
||||
let path = AbsolutePathBuf::from_absolute_path_checked(path.as_str())
|
||||
.wrap_err_with(|| format!("invalid local app-server fs path {path}"))?;
|
||||
self.client
|
||||
.request_typed(local_request(request_id, path))
|
||||
.await
|
||||
.wrap_err_with(|| format!("{method} failed in TUI"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user