Add a gated Cloud backend to the TUI (#59828)

## What changed

- Register an additional Cloud connection on supported macOS and Linux launches when `TuiMultiBackend` is enabled, using the selected ChatGPT account and local network policy.
- Preserve backend settings and identify Cloud in connection controls and status.
- Let Cloud supply the model when a thread opens, skip local skills requests and stale results, and allow resume without a local working-directory override.
- Preserve Git metadata in Cloud thread snapshots and continue history hydration past empty filtered pages within the scan budget.
- Report Cloud connection failures and require reopening Codex to reconnect.

## Testing

Add regression coverage for Cloud model bootstrap, skipped skills requests, resume directory handling, history pagination through filtered pages, and Git metadata preservation.

GitOrigin-RevId: 6717f0f5ad7708c5e9b5796d94711dd1eecc49bb
This commit is contained in:
Benjamin Carlsson
2026-11-06 05:26:52 +00:00
committed by copyberry
parent a037e65ead
commit 7b8d67e456
26 changed files with 484 additions and 123 deletions
+15
View File
@@ -79,6 +79,7 @@ pub struct ThreadSnapshot {
pub thread_source: Option<String>,
pub agent_nickname: Option<String>,
pub agent_role: Option<String>,
pub git_info: Option<GitInfo>,
pub name: Option<String>,
pub environments: Option<Vec<ThreadEnvironment>>,
/// Activation deliberately excludes turns; callers page history separately.
@@ -87,6 +88,15 @@ pub struct ThreadSnapshot {
raw_protobuf: Bytes,
}
/// Optional repository metadata associated with a cloud conversation.
#[derive(Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GitInfo {
pub branch: Option<String>,
pub origin_url: Option<String>,
pub sha: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ThreadHistoryMode {
@@ -343,6 +353,11 @@ impl ThreadSnapshot {
thread_source: thread.thread_source,
agent_nickname: thread.agent_nickname,
agent_role: thread.agent_role,
git_info: thread.git_info.map(|git_info| GitInfo {
branch: git_info.branch,
origin_url: git_info.origin_url,
sha: git_info.sha,
}),
name: thread.name,
environments: thread
.environments
@@ -203,6 +203,8 @@ pub(super) struct Thread {
pub ephemeral: Option<bool>,
#[prost(string, optional, tag = "11")]
pub forked_from_id: Option<String>,
#[prost(message, optional, tag = "12")]
pub git_info: Option<GitInfo>,
#[prost(int32, optional, tag = "13")]
pub history_mode: Option<i32>,
#[prost(string, optional, tag = "14")]
@@ -245,6 +247,23 @@ impl fmt::Debug for Thread {
}
}
#[derive(Clone, Message)]
#[prost(skip_debug)]
pub(super) struct GitInfo {
#[prost(string, optional, tag = "1")]
pub branch: Option<String>,
#[prost(string, optional, tag = "2")]
pub origin_url: Option<String>,
#[prost(string, optional, tag = "3")]
pub sha: Option<String>,
}
impl fmt::Debug for GitInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("GitInfo([redacted])")
}
}
#[derive(Clone, Message)]
pub(super) struct Environments {
#[prost(message, repeated, tag = "1")]
+11 -2
View File
@@ -48,8 +48,11 @@ async fn discovery_preserves_pagination_and_order_on_each_request() {
#[tokio::test]
async fn discovery_preserves_native_snapshots_and_read_state() {
// Independent native Thread encodings: modern future field (99) and legacy empty metadata.
let thread = Bytes::from_static(b"\x22\x01v\x28\x01\x32\x01/\x48\x00\x72\x01t\x82\x01\x01p\xaa\x01\x00\xda\x01\x01s\xe2\x01\x02\x08\x63\xea\x01\x02\x12\x00\xfa\x01\x00\x80\x02\x02\x98\x06\x01");
// Independent native Thread encodings: Git metadata and future field (99), and a legacy row.
let thread = Bytes::from_static(
b"\x22\x01v\x28\x01\x32\x01/\x48\x00\x62\x28\x0a\x04main\x12\x18https://example.com/repo\x1a\x06abc123\
\x72\x01t\x82\x01\x01p\xaa\x01\x00\xda\x01\x01s\xe2\x01\x02\x08\x63\xea\x01\x02\x12\x00\xfa\x01\x00\x80\x02\x02\x98\x06\x01",
);
let legacy = Bytes::from_static(b"\x22\x00\x28\x00\x32\x01/\x48\x00\x72\x06legacy\x82\x01\x00\xaa\x01\x00\xda\x01\x00\xe2\x01\x02\x08\x01\xea\x01\x02\x12\x00\xfa\x01\x00\x80\x02\x00");
let mut data = Vec::new();
for row in [&thread, &legacy, &thread] {
@@ -81,6 +84,12 @@ async fn discovery_preserves_native_snapshots_and_read_state() {
),
("legacy", "", "", "")
);
let page_json = serde_json::to_value(&page).unwrap();
assert_eq!(
page_json["data"][0]["gitInfo"],
serde_json::json!({"branch": "main", "originUrl": "https://example.com/repo", "sha": "abc123"})
);
assert!(page.data[1].git_info.is_none());
assert_eq!(page.data[0].status, ThreadStatus::Idle);
assert_eq!(
page.read_states,
+1
View File
@@ -24,6 +24,7 @@ pub use activation::ApprovalPolicy;
pub use activation::ApprovalsReviewer;
pub use activation::CollaborationMode;
pub use activation::CollaborationSettings;
pub use activation::GitInfo;
pub use activation::NetworkAccess;
pub use activation::SandboxPolicy;
pub use activation::SessionSource;