mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Add thread/settings/update app-server API (#23502)
## Why App-server clients need a way to update a thread's next-turn settings without starting a turn, adding transcript content, or waiting for turn lifecycle events. This gives settings UI a direct path for durable thread settings while clients observe the eventual effective state through a notification. This is a simplified rework of PR https://github.com/openai/codex/pull/22509. In particular, it changes the `thread/settings/update` api to return immediately rather than waiting and returning the effective (updated) thread settings. This makes the new api consistent with `turn/start` and greatly reduces the complexity of the implementation relative to the earlier attempt. ## What Changed - Adds experimental `thread/settings/update` with partial-update request fields and an empty acknowledgment response. - Adds experimental `thread/settings/updated`, carrying full effective `ThreadSettings` and scoped by `threadId` to subscribed clients for the affected thread. - Shares durable settings validation with `turn/start`, including `sandboxPolicy` plus `permissions` rejection and `serviceTier: null` clearing. - Emits the same settings notification when `turn/start` overrides change the stored effective thread settings. - Regenerates app-server protocol schema fixtures and updates `app-server/README.md`.
This commit is contained in:
1 parent
2b4898cc47
commit
771a4e74ac
27 files changed
+2092
-159
No files matched your search
@@ -237,12 +237,19 @@ pub(super) async fn ensure_listener_task_running(
|
||||
&environments,
|
||||
)
|
||||
.await;
|
||||
let thread_settings_baseline =
|
||||
thread_settings_from_config_snapshot(&conversation.config_snapshot().await);
|
||||
let (mut listener_command_rx, listener_generation) = {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
if thread_state.listener_matches(&conversation) {
|
||||
return Ok(());
|
||||
}
|
||||
thread_state.set_listener(cancel_tx, &conversation, watch_registration)
|
||||
thread_state.set_listener(
|
||||
cancel_tx,
|
||||
&conversation,
|
||||
watch_registration,
|
||||
thread_settings_baseline,
|
||||
)
|
||||
};
|
||||
let ListenerTaskContext {
|
||||
outgoing,
|
||||
|
||||
@@ -169,13 +169,13 @@ pub(super) fn with_thread_spawn_agent_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn thread_response_active_permission_profile(
|
||||
pub(crate) fn thread_response_active_permission_profile(
|
||||
active_permission_profile: Option<codex_protocol::models::ActivePermissionProfile>,
|
||||
) -> Option<codex_app_server_protocol::ActivePermissionProfile> {
|
||||
active_permission_profile.map(Into::into)
|
||||
}
|
||||
|
||||
pub(super) fn thread_response_sandbox_policy(
|
||||
pub(crate) fn thread_response_sandbox_policy(
|
||||
permission_profile: &codex_protocol::models::PermissionProfile,
|
||||
cwd: &Path,
|
||||
) -> codex_app_server_protocol::SandboxPolicy {
|
||||
@@ -189,6 +189,54 @@ pub(super) fn thread_response_sandbox_policy(
|
||||
sandbox_policy.into()
|
||||
}
|
||||
|
||||
pub(crate) fn thread_settings_from_config_snapshot(
|
||||
config_snapshot: &ThreadConfigSnapshot,
|
||||
) -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
cwd: config_snapshot.cwd.clone(),
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
|
||||
sandbox_policy: thread_response_sandbox_policy(
|
||||
&config_snapshot.permission_profile,
|
||||
config_snapshot.cwd.as_path(),
|
||||
),
|
||||
active_permission_profile: thread_response_active_permission_profile(
|
||||
config_snapshot.active_permission_profile.clone(),
|
||||
),
|
||||
model: config_snapshot.model.clone(),
|
||||
model_provider: config_snapshot.model_provider_id.clone(),
|
||||
service_tier: config_snapshot.service_tier.clone(),
|
||||
effort: config_snapshot.reasoning_effort,
|
||||
summary: config_snapshot.reasoning_summary,
|
||||
collaboration_mode: config_snapshot.collaboration_mode.clone(),
|
||||
personality: config_snapshot.personality,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn thread_settings_from_core_snapshot(
|
||||
snapshot: codex_protocol::protocol::ThreadSettingsSnapshot,
|
||||
) -> ThreadSettings {
|
||||
ThreadSettings {
|
||||
sandbox_policy: thread_response_sandbox_policy(
|
||||
&snapshot.permission_profile,
|
||||
snapshot.cwd.as_path(),
|
||||
),
|
||||
cwd: snapshot.cwd,
|
||||
approval_policy: snapshot.approval_policy.into(),
|
||||
approvals_reviewer: snapshot.approvals_reviewer.into(),
|
||||
active_permission_profile: thread_response_active_permission_profile(
|
||||
snapshot.active_permission_profile,
|
||||
),
|
||||
model: snapshot.model,
|
||||
model_provider: snapshot.model_provider_id,
|
||||
service_tier: snapshot.service_tier,
|
||||
effort: snapshot.reasoning_effort,
|
||||
summary: snapshot.reasoning_summary,
|
||||
collaboration_mode: snapshot.collaboration_mode,
|
||||
personality: snapshot.personality,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn parse_datetime(timestamp: Option<&str>) -> Option<DateTime<Utc>> {
|
||||
timestamp.and_then(|ts| {
|
||||
|
||||
@@ -30,6 +30,22 @@ fn resolve_runtime_workspace_roots(
|
||||
resolved_roots
|
||||
}
|
||||
|
||||
struct ThreadSettingsBuildParams {
|
||||
method: &'static str,
|
||||
cwd: Option<PathBuf>,
|
||||
runtime_workspace_roots: Option<Vec<PathBuf>>,
|
||||
approval_policy: Option<codex_app_server_protocol::AskForApproval>,
|
||||
approvals_reviewer: Option<codex_app_server_protocol::ApprovalsReviewer>,
|
||||
sandbox_policy: Option<codex_app_server_protocol::SandboxPolicy>,
|
||||
permissions: Option<String>,
|
||||
model: Option<String>,
|
||||
service_tier: Option<Option<String>>,
|
||||
effort: Option<ReasoningEffort>,
|
||||
summary: Option<ReasoningSummary>,
|
||||
collaboration_mode: Option<CollaborationMode>,
|
||||
personality: Option<Personality>,
|
||||
}
|
||||
|
||||
impl TurnRequestProcessor {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
@@ -88,6 +104,16 @@ impl TurnRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_settings_update(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadSettingsUpdateParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
self.thread_settings_update_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
pub(crate) async fn turn_steer(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
@@ -199,7 +225,7 @@ impl TurnRequestProcessor {
|
||||
|
||||
Ok((thread_id, thread))
|
||||
}
|
||||
fn normalize_turn_start_collaboration_mode(
|
||||
fn normalize_collaboration_mode(
|
||||
&self,
|
||||
mut collaboration_mode: CollaborationMode,
|
||||
) -> CollaborationMode {
|
||||
@@ -357,9 +383,6 @@ impl TurnRequestProcessor {
|
||||
self.track_error_response(&request_id, error, /*error_type*/ None);
|
||||
})?;
|
||||
|
||||
let collaboration_mode = params
|
||||
.collaboration_mode
|
||||
.map(|mode| self.normalize_turn_start_collaboration_mode(mode));
|
||||
let environment_selections = self.parse_environment_selections(params.environments)?;
|
||||
|
||||
// Map v2 input items to core input items.
|
||||
@@ -369,156 +392,26 @@ impl TurnRequestProcessor {
|
||||
.map(V2UserInput::into_core)
|
||||
.collect();
|
||||
let turn_has_input = !mapped_items.is_empty();
|
||||
let runtime_workspace_roots_request = params.runtime_workspace_roots.clone();
|
||||
let snapshot = if params.permissions.is_some() || runtime_workspace_roots_request.is_some()
|
||||
{
|
||||
Some(thread.config_snapshot().await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let has_any_overrides = params.cwd.is_some()
|
||||
|| runtime_workspace_roots_request.is_some()
|
||||
|| params.approval_policy.is_some()
|
||||
|| params.approvals_reviewer.is_some()
|
||||
|| params.sandbox_policy.is_some()
|
||||
|| params.permissions.is_some()
|
||||
|| params.model.is_some()
|
||||
|| params.service_tier.is_some()
|
||||
|| params.effort.is_some()
|
||||
|| params.summary.is_some()
|
||||
|| collaboration_mode.is_some()
|
||||
|| params.personality.is_some();
|
||||
|
||||
if params.sandbox_policy.is_some() && params.permissions.is_some() {
|
||||
return Err(invalid_request(
|
||||
"`permissions` cannot be combined with `sandboxPolicy`",
|
||||
));
|
||||
}
|
||||
|
||||
let cwd = params.cwd;
|
||||
let runtime_workspace_roots = if let Some(workspace_roots) =
|
||||
runtime_workspace_roots_request.clone()
|
||||
{
|
||||
let Some(snapshot) = snapshot.as_ref() else {
|
||||
return Err(internal_error(
|
||||
"turn/start runtime workspace roots missing thread snapshot",
|
||||
));
|
||||
};
|
||||
let base_cwd = cwd
|
||||
.as_ref()
|
||||
.map(|cwd| AbsolutePathBuf::resolve_path_against_base(cwd, snapshot.cwd.as_path()))
|
||||
.unwrap_or_else(|| snapshot.cwd.clone());
|
||||
Some(resolve_runtime_workspace_roots(workspace_roots, &base_cwd))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let approval_policy = params.approval_policy.map(AskForApproval::to_core);
|
||||
let approvals_reviewer = params
|
||||
.approvals_reviewer
|
||||
.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
|
||||
let sandbox_policy = params.sandbox_policy.map(|p| p.to_core());
|
||||
let (permission_profile, active_permission_profile, profile_workspace_roots) =
|
||||
if let Some(permissions) = params.permissions {
|
||||
let Some(snapshot) = snapshot.as_ref() else {
|
||||
return Err(internal_error(
|
||||
"turn/start permission selection missing thread snapshot",
|
||||
));
|
||||
};
|
||||
let overrides = ConfigOverrides {
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: Some(runtime_workspace_roots_request.clone().unwrap_or_else(
|
||||
|| {
|
||||
snapshot
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect()
|
||||
},
|
||||
)),
|
||||
default_permissions: Some(permissions),
|
||||
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let config = self
|
||||
.config_manager
|
||||
.load_for_cwd(
|
||||
/*request_overrides*/ None,
|
||||
overrides,
|
||||
Some(snapshot.cwd.to_path_buf()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
// Startup config is allowed to fall back when requirements
|
||||
// disallow a configured profile. An explicit turn request
|
||||
// is different: reject it before accepting user input.
|
||||
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
|
||||
warning.contains("Configured value for `permission_profile` is disallowed")
|
||||
}) {
|
||||
return Err(invalid_request(format!(
|
||||
"invalid thread settings override: {warning}"
|
||||
)));
|
||||
}
|
||||
(
|
||||
Some(config.permissions.permission_profile().clone()),
|
||||
config.permissions.active_permission_profile(),
|
||||
Some(config.permissions.profile_workspace_roots().to_vec()),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
let model = params.model;
|
||||
let effort = params.effort.map(Some);
|
||||
let summary = params.summary;
|
||||
let service_tier = params.service_tier;
|
||||
let personality = params.personality;
|
||||
|
||||
// If any overrides are provided, validate them synchronously so the
|
||||
// request can fail before accepting user input. The actual update is
|
||||
// still queued together with the input below to preserve submission order.
|
||||
if has_any_overrides {
|
||||
thread
|
||||
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: runtime_workspace_roots.clone(),
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: active_permission_profile.clone(),
|
||||
profile_workspace_roots: profile_workspace_roots.clone(),
|
||||
windows_sandbox_level: None,
|
||||
model: model.clone(),
|
||||
effort,
|
||||
summary,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
personality,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
invalid_request(format!("invalid thread settings override: {err}"))
|
||||
})?;
|
||||
}
|
||||
|
||||
let thread_settings = codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd,
|
||||
workspace_roots: runtime_workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level: None,
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
};
|
||||
let thread_settings = self
|
||||
.build_thread_settings_overrides(
|
||||
thread.as_ref(),
|
||||
ThreadSettingsBuildParams {
|
||||
method: "turn/start",
|
||||
cwd: params.cwd,
|
||||
runtime_workspace_roots: params.runtime_workspace_roots,
|
||||
approval_policy: params.approval_policy,
|
||||
approvals_reviewer: params.approvals_reviewer,
|
||||
sandbox_policy: params.sandbox_policy,
|
||||
permissions: params.permissions,
|
||||
model: params.model,
|
||||
service_tier: params.service_tier,
|
||||
effort: params.effort,
|
||||
summary: params.summary,
|
||||
collaboration_mode: params.collaboration_mode,
|
||||
personality: params.personality,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Start the turn by submitting the user input. Return its submission id as turn_id.
|
||||
let turn_op = Op::UserInput {
|
||||
@@ -566,6 +459,215 @@ impl TurnRequestProcessor {
|
||||
Ok(TurnStartResponse { turn })
|
||||
}
|
||||
|
||||
async fn build_thread_settings_overrides(
|
||||
&self,
|
||||
thread: &CodexThread,
|
||||
params: ThreadSettingsBuildParams,
|
||||
) -> Result<codex_protocol::protocol::ThreadSettingsOverrides, JSONRPCErrorError> {
|
||||
let ThreadSettingsBuildParams {
|
||||
method,
|
||||
cwd,
|
||||
runtime_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permissions,
|
||||
model,
|
||||
service_tier,
|
||||
effort,
|
||||
summary,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
} = params;
|
||||
|
||||
if sandbox_policy.is_some() && permissions.is_some() {
|
||||
return Err(invalid_request(
|
||||
"`permissions` cannot be combined with `sandboxPolicy`",
|
||||
));
|
||||
}
|
||||
|
||||
let collaboration_mode =
|
||||
collaboration_mode.map(|mode| self.normalize_collaboration_mode(mode));
|
||||
let runtime_workspace_roots_request = runtime_workspace_roots;
|
||||
// `thread/settings/update` only acknowledges that the update was queued.
|
||||
// Clients that send dependent partial updates should wait for
|
||||
// `thread/settings/updated` or combine the fields in one request.
|
||||
let snapshot = if permissions.is_some() || runtime_workspace_roots_request.is_some() {
|
||||
Some(thread.config_snapshot().await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let has_any_overrides = cwd.is_some()
|
||||
|| runtime_workspace_roots_request.is_some()
|
||||
|| approval_policy.is_some()
|
||||
|| approvals_reviewer.is_some()
|
||||
|| sandbox_policy.is_some()
|
||||
|| permissions.is_some()
|
||||
|| model.is_some()
|
||||
|| service_tier.is_some()
|
||||
|| effort.is_some()
|
||||
|| summary.is_some()
|
||||
|| collaboration_mode.is_some()
|
||||
|| personality.is_some();
|
||||
|
||||
let runtime_workspace_roots = if let Some(workspace_roots) =
|
||||
runtime_workspace_roots_request.clone()
|
||||
{
|
||||
let Some(snapshot) = snapshot.as_ref() else {
|
||||
return Err(internal_error(format!(
|
||||
"{method} runtime workspace roots missing thread snapshot"
|
||||
)));
|
||||
};
|
||||
let base_cwd = cwd
|
||||
.as_ref()
|
||||
.map(|cwd| AbsolutePathBuf::resolve_path_against_base(cwd, snapshot.cwd.as_path()))
|
||||
.unwrap_or_else(|| snapshot.cwd.clone());
|
||||
Some(resolve_runtime_workspace_roots(workspace_roots, &base_cwd))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let approval_policy =
|
||||
approval_policy.map(codex_app_server_protocol::AskForApproval::to_core);
|
||||
let approvals_reviewer =
|
||||
approvals_reviewer.map(codex_app_server_protocol::ApprovalsReviewer::to_core);
|
||||
let sandbox_policy = sandbox_policy.map(|policy| policy.to_core());
|
||||
let (permission_profile, active_permission_profile, profile_workspace_roots) =
|
||||
if let Some(permissions) = permissions {
|
||||
let Some(snapshot) = snapshot.as_ref() else {
|
||||
return Err(internal_error(format!(
|
||||
"{method} permission selection missing thread snapshot"
|
||||
)));
|
||||
};
|
||||
let overrides = ConfigOverrides {
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: Some(runtime_workspace_roots_request.clone().unwrap_or_else(
|
||||
|| {
|
||||
snapshot
|
||||
.workspace_roots
|
||||
.iter()
|
||||
.map(AbsolutePathBuf::to_path_buf)
|
||||
.collect()
|
||||
},
|
||||
)),
|
||||
default_permissions: Some(permissions),
|
||||
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let config = self
|
||||
.config_manager
|
||||
.load_for_cwd(
|
||||
/*request_overrides*/ None,
|
||||
overrides,
|
||||
Some(snapshot.cwd.to_path_buf()),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
// Startup config is allowed to fall back when requirements
|
||||
// disallow a configured profile. An explicit settings update
|
||||
// is different: reject it before accepting the request.
|
||||
if let Some(warning) = config.startup_warnings.iter().find(|warning| {
|
||||
warning.contains("Configured value for `permission_profile` is disallowed")
|
||||
}) {
|
||||
return Err(invalid_request(format!(
|
||||
"invalid thread settings override: {warning}"
|
||||
)));
|
||||
}
|
||||
(
|
||||
Some(config.permissions.permission_profile().clone()),
|
||||
config.permissions.active_permission_profile(),
|
||||
Some(config.permissions.profile_workspace_roots().to_vec()),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
let effort = effort.map(Some);
|
||||
|
||||
if has_any_overrides {
|
||||
thread
|
||||
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
|
||||
cwd: cwd.clone(),
|
||||
workspace_roots: runtime_workspace_roots.clone(),
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: active_permission_profile.clone(),
|
||||
profile_workspace_roots: profile_workspace_roots.clone(),
|
||||
windows_sandbox_level: None,
|
||||
model: model.clone(),
|
||||
effort,
|
||||
summary,
|
||||
service_tier: service_tier.clone(),
|
||||
collaboration_mode: collaboration_mode.clone(),
|
||||
personality,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| {
|
||||
invalid_request(format!("invalid thread settings override: {err}"))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(codex_protocol::protocol::ThreadSettingsOverrides {
|
||||
cwd,
|
||||
workspace_roots: runtime_workspace_roots,
|
||||
profile_workspace_roots,
|
||||
approval_policy,
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level: None,
|
||||
model,
|
||||
effort,
|
||||
summary,
|
||||
service_tier,
|
||||
collaboration_mode,
|
||||
personality,
|
||||
})
|
||||
}
|
||||
|
||||
async fn thread_settings_update_inner(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
params: ThreadSettingsUpdateParams,
|
||||
) -> Result<ThreadSettingsUpdateResponse, JSONRPCErrorError> {
|
||||
let (_, thread) = self.load_thread(¶ms.thread_id).await?;
|
||||
let thread_settings = self
|
||||
.build_thread_settings_overrides(
|
||||
thread.as_ref(),
|
||||
ThreadSettingsBuildParams {
|
||||
method: "thread/settings/update",
|
||||
cwd: params.cwd,
|
||||
runtime_workspace_roots: None,
|
||||
approval_policy: params.approval_policy,
|
||||
approvals_reviewer: params.approvals_reviewer,
|
||||
sandbox_policy: params.sandbox_policy,
|
||||
permissions: params.permissions,
|
||||
model: params.model,
|
||||
service_tier: params.service_tier,
|
||||
effort: params.effort,
|
||||
summary: params.summary,
|
||||
collaboration_mode: params.collaboration_mode,
|
||||
personality: params.personality,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
if thread_settings != codex_protocol::protocol::ThreadSettingsOverrides::default() {
|
||||
self.submit_core_op(
|
||||
request_id,
|
||||
thread.as_ref(),
|
||||
Op::ThreadSettings { thread_settings },
|
||||
)
|
||||
.await
|
||||
.map_err(|err| internal_error(format!("failed to update thread settings: {err}")))?;
|
||||
}
|
||||
|
||||
Ok(ThreadSettingsUpdateResponse {})
|
||||
}
|
||||
|
||||
async fn thread_inject_items_response_inner(
|
||||
&self,
|
||||
params: ThreadInjectItemsParams,
|
||||
|
||||
Reference in new issue
Block a user