[2 of 4] tui: route app and skill enablement through app server (#22914)

## Why
App and skill toggles are user config mutations too. When the TUI is
attached to a remote app server, writing those toggles into the local
`config.toml` makes the UI report success without updating the server
that actually owns the session.

This is **[2 of 4]** in a stacked series that moves TUI-owned config
mutations onto app-server APIs.

## What changed
- Routed app enable/disable persistence through app-server config batch
writes.
- Routed skill enable/disable persistence through `skills/config/write`.
- Avoided refreshing local config from disk after these writes when the
TUI is connected to a remote app server.

## Config keys affected
- `apps.<app_id>.enabled`
- `apps.<app_id>.disabled_reason`
- `[[skills.config]]` entries keyed by `path`, with `enabled = false`
used for persisted disables

## Suggested manual validation
- Connect the TUI to a remote app server, disable an app, reconnect, and
confirm the app remains disabled from remote config rather than local
disk state.
- Re-enable the same app and confirm both `apps.<app_id>.enabled` and
`apps.<app_id>.disabled_reason` are cleared remotely.
- Disable a skill in the manage-skills UI and confirm a remote
`[[skills.config]]` disable entry appears.
- Re-enable that skill and confirm the disable entry is removed and the
effective enabled state updates without relying on local config reloads.

## Stack
1. [#22913](https://github.com/openai/codex/pull/22913) `[1 of 4]`
primary settings writes
2. [#22914](https://github.com/openai/codex/pull/22914) `[2 of 4]` app
and skill enablement
3. [#22915](https://github.com/openai/codex/pull/22915) `[3 of 4]`
feature and memory toggles
4. [#22916](https://github.com/openai/codex/pull/22916) `[4 of 4]`
startup and onboarding bookkeeping
This commit is contained in:
Eric Traut
2026-05-19 10:21:07 -07:00
committed by GitHub
parent f0663fd4fd
commit ae10708ae0
18 changed files with 189 additions and 326 deletions

View File

@@ -275,6 +275,7 @@ use codex_core::config::ConfigOverrides;
use codex_core::config::NetworkProxyAuditMetadata;
use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::connectors::AccessibleConnectorsStatus;
use codex_core::exec::ExecCapturePolicy;
use codex_core::exec::ExecExpiration;
use codex_core::exec::ExecParams;

View File

@@ -41,11 +41,19 @@ impl AppsRequestProcessor {
request_id: &ConnectionRequestId,
params: AppsListParams,
) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {
let mut config = self.load_latest_config(/*fallback_cwd*/ None).await?;
if let Some(thread_id) = params.thread_id.as_deref() {
let (_, thread) = self.load_thread(thread_id).await?;
let thread = if let Some(thread_id) = params.thread_id.as_deref() {
let (_, loaded_thread) = self.load_thread(thread_id).await?;
Some(loaded_thread)
} else {
None
};
let fallback_cwd = match thread.as_ref() {
Some(thread) => Some(thread.config_snapshot().await.cwd.to_path_buf()),
None => None,
};
let mut config = self.load_latest_config(fallback_cwd).await?;
if let Some(thread) = thread {
let _ = config
.features
.set_enabled(Feature::Apps, thread.enabled(Feature::Apps));
@@ -88,8 +96,31 @@ impl AppsRequestProcessor {
config: Config,
environment_manager: Arc<EnvironmentManager>,
) {
let retry_params = params.clone();
let retry_config = config.clone();
let retry_environment_manager = Arc::clone(&environment_manager);
let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await;
outgoing.send_result(request_id, result).await;
let should_retry = result
.as_ref()
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
outgoing
.send_result(request_id, result.map(|(response, _)| response))
.await;
if should_retry && !retry_params.force_refetch {
let mut retry_params = retry_params;
retry_params.force_refetch = true;
if let Err(err) = Self::apps_list_response(
&outgoing,
retry_params,
retry_config,
retry_environment_manager,
)
.await
{
warn!("failed to refresh app list after codex-apps readiness retry: {err:?}");
}
}
}
async fn apps_list_response(
@@ -97,7 +128,7 @@ impl AppsRequestProcessor {
params: AppsListParams,
config: Config,
environment_manager: Arc<EnvironmentManager>,
) -> Result<AppsListResponse, JSONRPCErrorError> {
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
let AppsListParams {
cursor,
limit,
@@ -130,7 +161,6 @@ impl AppsRequestProcessor {
&environment_manager,
)
.await
.map(|status| status.connectors)
.map_err(|err| format!("failed to load accessible apps: {err}"));
let _ = accessible_tx.send(AppListLoadResult::Accessible(result));
});
@@ -146,6 +176,7 @@ impl AppsRequestProcessor {
let app_list_deadline = tokio::time::Instant::now() + APP_LIST_LOAD_TIMEOUT;
let mut accessible_loaded = false;
let mut all_loaded = false;
let mut codex_apps_ready = true;
let mut last_notified_apps = None;
if accessible_connectors.is_some() || all_connectors.is_some() {
@@ -178,9 +209,10 @@ impl AppsRequestProcessor {
};
match result {
AppListLoadResult::Accessible(Ok(connectors)) => {
accessible_connectors = Some(connectors);
AppListLoadResult::Accessible(Ok(status)) => {
accessible_connectors = Some(status.connectors);
accessible_loaded = true;
codex_apps_ready = status.codex_apps_ready;
}
AppListLoadResult::Accessible(Err(err)) => {
return Err(internal_error(err));
@@ -222,7 +254,8 @@ impl AppsRequestProcessor {
}
if accessible_loaded && all_loaded {
return paginate_apps(merged.as_slice(), start, limit);
let response = paginate_apps(merged.as_slice(), start, limit)?;
return Ok((response, codex_apps_ready));
}
}
}
@@ -279,7 +312,7 @@ impl AppsRequestProcessor {
const APP_LIST_LOAD_TIMEOUT: Duration = Duration::from_secs(90);
enum AppListLoadResult {
Accessible(Result<Vec<AppInfo>, String>),
Accessible(Result<AccessibleConnectorsStatus, String>),
Directory(Result<Vec<AppInfo>, String>),
}