Files
codex/codex-rs/app-server-client/src/path.rs
T
Eric Traut 78bab04116 [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.
2026-06-11 22:26:31 -07:00

59 lines
1.6 KiB
Rust

//! Paths resolved using the app-server host's platform rules.
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AppServerPath(String);
impl AppServerPath {
pub fn from_app_server(path: impl Into<String>) -> Self {
Self(path.into())
}
pub fn from_absolute_str(raw: &str) -> Option<Self> {
(raw.starts_with('/') || is_windows_absolute_path(raw)).then(|| Self(raw.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn components(&self) -> Vec<&str> {
let separators = if is_windows_absolute_path(&self.0) {
&['/', '\\'][..]
} else {
&['/'][..]
};
self.0
.split(separators)
.filter(|part| !part.is_empty())
.collect()
}
pub fn join(&self, segment: impl AsRef<str>) -> Self {
let is_windows = is_windows_absolute_path(&self.0);
let (path, separator) = if is_windows {
(self.0.trim_end_matches(['/', '\\']), '\\')
} else {
(self.0.trim_end_matches('/'), '/')
};
Self(format!("{path}{separator}{}", segment.as_ref()))
}
}
impl fmt::Display for AppServerPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
fn is_windows_absolute_path(path: &str) -> bool {
let bytes = path.as_bytes();
(bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'\\' | b'/'))
|| path.starts_with("\\\\")
|| path.starts_with("//")
}