mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Expose instruction sources (AGENTS.md) via app server (#17506)
Addresses #17498 Problem: The TUI derived /status instruction source paths from the local client environment, which could show stale <none> output or incorrect paths when connected to a remote app server. Solution: Add an app-server v2 instructionSources snapshot to thread start/resume/fork responses, default it to an empty list when older servers omit it, and render TUI /status from that server-provided session data. Additional context: The app-server field is intentionally named instructionSources rather than AGENTS.md-specific terminology because the loaded instruction sources can include global instructions, project AGENTS.md files, AGENTS.override.md, user-defined instruction files, and future dynamic sources.
This commit is contained in:
committed by
GitHub
Unverified
parent
470510174b
commit
46ab9974dc
@@ -105,6 +105,7 @@ fn sample_thread_start_response(thread_id: &str, ephemeral: bool, model: &str) -
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
@@ -155,6 +156,7 @@ fn sample_thread_resume_response_with_source(
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: AppServerAskForApproval::OnFailure,
|
||||
approvals_reviewer: AppServerApprovalsReviewer::User,
|
||||
sandbox: AppServerSandboxPolicy::DangerFullAccess,
|
||||
|
||||
@@ -12806,6 +12806,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -14095,6 +14103,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -14386,6 +14402,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -10654,6 +10654,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -11943,6 +11951,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -12234,6 +12250,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2187,6 +2187,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2187,6 +2187,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2187,6 +2187,14 @@
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"instructionSources": {
|
||||
"default": [],
|
||||
"description": "Instruction source files currently loaded for this thread.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,11 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval,
|
||||
export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string,
|
||||
/**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<string>, approvalPolicy: AskForApproval,
|
||||
/**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,11 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval,
|
||||
export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string,
|
||||
/**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<string>, approvalPolicy: AskForApproval,
|
||||
/**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,11 @@ import type { AskForApproval } from "./AskForApproval";
|
||||
import type { SandboxPolicy } from "./SandboxPolicy";
|
||||
import type { Thread } from "./Thread";
|
||||
|
||||
export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string, approvalPolicy: AskForApproval,
|
||||
export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: string,
|
||||
/**
|
||||
* Instruction source files currently loaded for this thread.
|
||||
*/
|
||||
instructionSources: Array<string>, approvalPolicy: AskForApproval,
|
||||
/**
|
||||
* Reviewer currently used for approval requests on this thread.
|
||||
*/
|
||||
|
||||
@@ -1405,6 +1405,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
instruction_sources: vec![PathBuf::from("/tmp/AGENTS.md")],
|
||||
approval_policy: v2::AskForApproval::OnFailure,
|
||||
approvals_reviewer: v2::ApprovalsReviewer::User,
|
||||
sandbox: v2::SandboxPolicy::DangerFullAccess,
|
||||
@@ -1444,6 +1445,7 @@ mod tests {
|
||||
"modelProvider": "openai",
|
||||
"serviceTier": null,
|
||||
"cwd": "/tmp",
|
||||
"instructionSources": ["/tmp/AGENTS.md"],
|
||||
"approvalPolicy": "on-failure",
|
||||
"approvalsReviewer": "user",
|
||||
"sandbox": {
|
||||
|
||||
@@ -2719,6 +2719,9 @@ pub struct ThreadStartResponse {
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
pub instruction_sources: Vec<PathBuf>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
@@ -2805,6 +2808,9 @@ pub struct ThreadResumeResponse {
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
pub instruction_sources: Vec<PathBuf>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
@@ -2882,6 +2888,9 @@ pub struct ThreadForkResponse {
|
||||
pub model_provider: String,
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
pub cwd: PathBuf,
|
||||
/// Instruction source files currently loaded for this thread.
|
||||
#[serde(default)]
|
||||
pub instruction_sources: Vec<PathBuf>,
|
||||
#[experimental(nested)]
|
||||
pub approval_policy: AskForApproval,
|
||||
/// Reviewer currently used for approval requests on this thread.
|
||||
@@ -8506,6 +8515,50 @@ mod tests {
|
||||
assert_eq!(serialized_without_override.get("serviceTier"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_lifecycle_responses_default_missing_instruction_sources() {
|
||||
let response = json!({
|
||||
"thread": {
|
||||
"id": "thread-id",
|
||||
"forkedFromId": null,
|
||||
"preview": "",
|
||||
"ephemeral": false,
|
||||
"modelProvider": "openai",
|
||||
"createdAt": 1,
|
||||
"updatedAt": 1,
|
||||
"status": { "type": "idle" },
|
||||
"path": null,
|
||||
"cwd": "/tmp",
|
||||
"cliVersion": "0.0.0",
|
||||
"source": "exec",
|
||||
"agentNickname": null,
|
||||
"agentRole": null,
|
||||
"gitInfo": null,
|
||||
"name": null,
|
||||
"turns": []
|
||||
},
|
||||
"model": "gpt-5",
|
||||
"modelProvider": "openai",
|
||||
"serviceTier": null,
|
||||
"cwd": "/tmp",
|
||||
"approvalPolicy": "on-failure",
|
||||
"approvalsReviewer": "user",
|
||||
"sandbox": { "type": "dangerFullAccess" },
|
||||
"reasoningEffort": null
|
||||
});
|
||||
|
||||
let start: ThreadStartResponse =
|
||||
serde_json::from_value(response.clone()).expect("thread/start response");
|
||||
let resume: ThreadResumeResponse =
|
||||
serde_json::from_value(response.clone()).expect("thread/resume response");
|
||||
let fork: ThreadForkResponse =
|
||||
serde_json::from_value(response).expect("thread/fork response");
|
||||
|
||||
assert_eq!(start.instruction_sources, Vec::<PathBuf>::new());
|
||||
assert_eq!(resume.instruction_sources, Vec::<PathBuf>::new());
|
||||
assert_eq!(fork.instruction_sources, Vec::<PathBuf>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
let params: TurnStartParams = serde_json::from_value(json!({
|
||||
|
||||
@@ -239,6 +239,7 @@ use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode;
|
||||
use codex_core::windows_sandbox::WindowsSandboxSetupRequest;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Stage;
|
||||
@@ -491,6 +492,23 @@ pub(crate) struct CodexMessageProcessorArgs {
|
||||
}
|
||||
|
||||
impl CodexMessageProcessor {
|
||||
async fn instruction_sources_from_config(config: &Config) -> Vec<PathBuf> {
|
||||
let mut paths: Vec<PathBuf> = config.user_instructions_path.iter().cloned().collect();
|
||||
match codex_core::discover_project_doc_paths(config, LOCAL_FS.as_ref()).await {
|
||||
Ok(project_doc_paths) => {
|
||||
paths.extend(
|
||||
project_doc_paths
|
||||
.into_iter()
|
||||
.map(|path| path.as_path().to_path_buf()),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to discover project docs for thread response");
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
pub(crate) fn handle_config_mutation(&self) {
|
||||
self.clear_plugin_related_caches();
|
||||
}
|
||||
@@ -2312,6 +2330,7 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
let dynamic_tools = dynamic_tools.unwrap_or_default();
|
||||
let core_dynamic_tools = if dynamic_tools.is_empty() {
|
||||
Vec::new()
|
||||
@@ -2443,6 +2462,7 @@ impl CodexMessageProcessor {
|
||||
model_provider: config_snapshot.model_provider_id,
|
||||
service_tier: config_snapshot.service_tier,
|
||||
cwd: config_snapshot.cwd,
|
||||
instruction_sources,
|
||||
approval_policy: config_snapshot.approval_policy.into(),
|
||||
approvals_reviewer: config_snapshot.approvals_reviewer.into(),
|
||||
sandbox: config_snapshot.sandbox_policy.into(),
|
||||
@@ -3881,6 +3901,7 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
let response_history = thread_history.clone();
|
||||
|
||||
match self
|
||||
@@ -3961,6 +3982,7 @@ impl CodexMessageProcessor {
|
||||
model_provider: session_configured.model_provider_id,
|
||||
service_tier: session_configured.service_tier,
|
||||
cwd: session_configured.cwd,
|
||||
instruction_sources,
|
||||
approval_policy: session_configured.approval_policy.into(),
|
||||
approvals_reviewer: session_configured.approvals_reviewer.into(),
|
||||
sandbox: session_configured.sandbox_policy.into(),
|
||||
@@ -4119,6 +4141,12 @@ impl CodexMessageProcessor {
|
||||
mismatch_details.join("; ")
|
||||
);
|
||||
}
|
||||
let mut config_for_instruction_sources = self.config.as_ref().clone();
|
||||
if let Ok(cwd) = AbsolutePathBuf::try_from(config_snapshot.cwd.clone()) {
|
||||
config_for_instruction_sources.cwd = cwd;
|
||||
}
|
||||
let instruction_sources =
|
||||
Self::instruction_sources_from_config(&config_for_instruction_sources).await;
|
||||
let thread_summary = match load_thread_summary_for_rollout(
|
||||
&self.config,
|
||||
existing_thread_id,
|
||||
@@ -4156,6 +4184,7 @@ impl CodexMessageProcessor {
|
||||
request_id: request_id.clone(),
|
||||
rollout_path: rollout_path.clone(),
|
||||
config_snapshot,
|
||||
instruction_sources,
|
||||
thread_summary,
|
||||
}),
|
||||
);
|
||||
@@ -4431,6 +4460,7 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
|
||||
let NewThread {
|
||||
thread_id,
|
||||
@@ -4585,6 +4615,7 @@ impl CodexMessageProcessor {
|
||||
model_provider: session_configured.model_provider_id,
|
||||
service_tier: session_configured.service_tier,
|
||||
cwd: session_configured.cwd,
|
||||
instruction_sources,
|
||||
approval_policy: session_configured.approval_policy.into(),
|
||||
approvals_reviewer: session_configured.approvals_reviewer.into(),
|
||||
sandbox: session_configured.sandbox_policy.into(),
|
||||
@@ -8156,12 +8187,14 @@ async fn handle_pending_thread_resume_request(
|
||||
reasoning_effort,
|
||||
..
|
||||
} = pending.config_snapshot;
|
||||
let instruction_sources = pending.instruction_sources;
|
||||
let response = ThreadResumeResponse {
|
||||
thread,
|
||||
model,
|
||||
model_provider: model_provider_id,
|
||||
service_tier,
|
||||
cwd,
|
||||
instruction_sources,
|
||||
approval_policy: approval_policy.into(),
|
||||
approvals_reviewer: approvals_reviewer.into(),
|
||||
sandbox: sandbox_policy.into(),
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) struct PendingThreadResumeRequest {
|
||||
pub(crate) request_id: ConnectionRequestId,
|
||||
pub(crate) rollout_path: PathBuf,
|
||||
pub(crate) config_snapshot: ThreadConfigSnapshot,
|
||||
pub(crate) instruction_sources: Vec<PathBuf>,
|
||||
pub(crate) thread_summary: codex_app_server_protocol::Thread,
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
@@ -162,6 +163,64 @@ async fn thread_start_creates_thread_and_emits_started() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_response_includes_loaded_instruction_sources() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
let global_agents_path = codex_home.path().join("AGENTS.md");
|
||||
std::fs::write(&global_agents_path, "global instructions")?;
|
||||
let workspace = TempDir::new()?;
|
||||
let project_agents_path = workspace.path().join("AGENTS.md");
|
||||
std::fs::write(&project_agents_path, "project instructions")?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
cwd: Some(workspace.path().display().to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse {
|
||||
instruction_sources,
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(response)?;
|
||||
|
||||
let instruction_sources = instruction_sources
|
||||
.into_iter()
|
||||
.map(normalize_path_for_comparison)
|
||||
.collect::<Vec<_>>();
|
||||
let expected_instruction_sources = vec![
|
||||
std::fs::canonicalize(global_agents_path)?,
|
||||
std::fs::canonicalize(project_agents_path)?,
|
||||
]
|
||||
.into_iter()
|
||||
.map(normalize_path_for_comparison)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(instruction_sources, expected_instruction_sources);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn normalize_path_for_comparison(path: PathBuf) -> PathBuf {
|
||||
let path = path.display().to_string();
|
||||
PathBuf::from(path.strip_prefix(r"\\?\").unwrap_or(&path))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn normalize_path_for_comparison(path: PathBuf) -> PathBuf {
|
||||
path
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_tracks_thread_initialized_analytics() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -410,6 +410,7 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() {
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
instruction_sources: Vec::new(),
|
||||
approval_policy: codex_app_server_protocol::AskForApproval::OnRequest,
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::GuardianSubagent,
|
||||
sandbox: codex_app_server_protocol::SandboxPolicy::WorkspaceWrite {
|
||||
|
||||
@@ -3062,6 +3062,7 @@ impl App {
|
||||
approvals_reviewer: self.config.approvals_reviewer,
|
||||
sandbox_policy: self.config.permissions.sandbox_policy.get().clone(),
|
||||
cwd: thread.cwd.clone(),
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: self.chat_widget.current_reasoning_effort(),
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
@@ -3072,6 +3073,7 @@ impl App {
|
||||
session.thread_name = thread.name.clone();
|
||||
session.model_provider_id = thread.model_provider.clone();
|
||||
session.cwd = thread.cwd.clone();
|
||||
session.instruction_source_paths = Vec::new();
|
||||
session.rollout_path = thread.path.clone();
|
||||
if let Some(model) =
|
||||
read_session_model(&self.config, thread_id, thread.path.as_deref()).await
|
||||
@@ -9365,6 +9367,7 @@ guardian_approval = true
|
||||
approvals_reviewer: ApprovalsReviewer::User,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
cwd,
|
||||
instruction_source_paths: Vec::new(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
|
||||
@@ -132,6 +132,7 @@ pub(crate) struct ThreadSessionState {
|
||||
pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
pub(crate) sandbox_policy: SandboxPolicy,
|
||||
pub(crate) cwd: PathBuf,
|
||||
pub(crate) instruction_source_paths: Vec<PathBuf>,
|
||||
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
pub(crate) history_log_id: u64,
|
||||
pub(crate) history_entry_count: u64,
|
||||
@@ -993,6 +994,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
response.approvals_reviewer.to_core(),
|
||||
response.sandbox.to_core(),
|
||||
response.cwd.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
)
|
||||
@@ -1015,6 +1017,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
response.approvals_reviewer.to_core(),
|
||||
response.sandbox.to_core(),
|
||||
response.cwd.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
)
|
||||
@@ -1037,6 +1040,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
response.approvals_reviewer.to_core(),
|
||||
response.sandbox.to_core(),
|
||||
response.cwd.clone(),
|
||||
response.instruction_sources.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
)
|
||||
@@ -1078,6 +1082,7 @@ async fn thread_session_state_from_thread_response(
|
||||
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
sandbox_policy: SandboxPolicy,
|
||||
cwd: PathBuf,
|
||||
instruction_source_paths: Vec<PathBuf>,
|
||||
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
config: &Config,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
@@ -1102,6 +1107,7 @@ async fn thread_session_state_from_thread_response(
|
||||
approvals_reviewer,
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
instruction_source_paths,
|
||||
reasoning_effort,
|
||||
history_log_id,
|
||||
history_entry_count,
|
||||
@@ -1326,6 +1332,7 @@ mod tests {
|
||||
model_provider: "openai".to_string(),
|
||||
service_tier: None,
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
instruction_sources: vec![PathBuf::from("/tmp/project/AGENTS.md")],
|
||||
approval_policy: codex_protocol::protocol::AskForApproval::Never.into(),
|
||||
approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::User,
|
||||
sandbox: codex_protocol::protocol::SandboxPolicy::new_read_only_policy().into(),
|
||||
@@ -1336,6 +1343,10 @@ mod tests {
|
||||
.await
|
||||
.expect("resume response should map");
|
||||
assert_eq!(started.session.forked_from_id, Some(forked_from_id));
|
||||
assert_eq!(
|
||||
started.session.instruction_source_paths,
|
||||
response.instruction_sources
|
||||
);
|
||||
assert_eq!(started.turns.len(), 1);
|
||||
assert_eq!(started.turns[0], response.thread.turns[0]);
|
||||
}
|
||||
@@ -1365,6 +1376,7 @@ mod tests {
|
||||
codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
PathBuf::from("/tmp/project"),
|
||||
Vec::new(),
|
||||
/*reasoning_effort*/ None,
|
||||
&config,
|
||||
)
|
||||
@@ -1394,6 +1406,7 @@ mod tests {
|
||||
codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
PathBuf::from("/tmp/project"),
|
||||
Vec::new(),
|
||||
/*reasoning_effort*/ None,
|
||||
&config,
|
||||
)
|
||||
|
||||
@@ -942,6 +942,8 @@ pub(crate) struct ChatWidget {
|
||||
current_rollout_path: Option<PathBuf>,
|
||||
// Current working directory (if known)
|
||||
current_cwd: Option<PathBuf>,
|
||||
// Instruction source files loaded for the current session, supplied by app-server.
|
||||
instruction_source_paths: Vec<PathBuf>,
|
||||
// Runtime network proxy bind addresses from SessionConfigured.
|
||||
session_network_proxy: Option<codex_protocol::protocol::SessionNetworkProxyRuntime>,
|
||||
// Shared latch so we only warn once about invalid status-line item IDs.
|
||||
@@ -2071,6 +2073,7 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
pub(crate) fn handle_thread_session(&mut self, session: ThreadSessionState) {
|
||||
self.instruction_source_paths = session.instruction_source_paths.clone();
|
||||
self.on_session_configured(thread_session_state_to_legacy_event(session));
|
||||
}
|
||||
|
||||
@@ -4865,6 +4868,7 @@ impl ChatWidget {
|
||||
feedback,
|
||||
current_rollout_path: None,
|
||||
current_cwd,
|
||||
instruction_source_paths: Vec::new(),
|
||||
session_network_proxy: None,
|
||||
status_line_invalid_items_warned,
|
||||
terminal_title_invalid_items_warned,
|
||||
@@ -7071,8 +7075,8 @@ impl ChatWidget {
|
||||
.values()
|
||||
.cloned()
|
||||
.collect();
|
||||
let config = self.config.clone();
|
||||
let frame_requester = self.frame_requester.clone();
|
||||
let agents_summary =
|
||||
crate::status::compose_agents_summary(&self.config, &self.instruction_source_paths);
|
||||
let (cell, handle) = crate::status::new_status_output_with_rate_limits_handle(
|
||||
&self.config,
|
||||
self.status_account_display.as_ref(),
|
||||
@@ -7087,21 +7091,9 @@ impl ChatWidget {
|
||||
self.model_display_name(),
|
||||
collaboration_mode,
|
||||
reasoning_effort_override,
|
||||
"<none>".to_string(),
|
||||
agents_summary,
|
||||
refreshing_rate_limits,
|
||||
);
|
||||
let agents_summary_handle = handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let agents_summary = match crate::status::discover_agents_summary(&config).await {
|
||||
Ok(summary) => summary,
|
||||
Err(err) => {
|
||||
tracing::warn!(error = %err, "failed to discover project docs for /status");
|
||||
"<none>".to_string()
|
||||
}
|
||||
};
|
||||
agents_summary_handle.finish_agents_summary_discovery(agents_summary);
|
||||
frame_requester.schedule_frame();
|
||||
});
|
||||
if let Some(request_id) = request_id {
|
||||
self.refreshing_status_outputs.push((request_id, handle));
|
||||
}
|
||||
|
||||
@@ -275,6 +275,7 @@ pub(super) async fn make_chatwidget_manual(
|
||||
feedback: codex_feedback::CodexFeedback::new(),
|
||||
current_rollout_path: None,
|
||||
current_cwd: None,
|
||||
instruction_source_paths: Vec::new(),
|
||||
session_network_proxy: None,
|
||||
status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
terminal_title_invalid_items_warned: Arc::new(AtomicBool::new(false)),
|
||||
|
||||
@@ -76,6 +76,29 @@ async fn status_command_renders_immediately_without_rate_limit_refresh() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_command_renders_instruction_sources_from_thread_session() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.instruction_source_paths = vec![chat.config.cwd.join("AGENTS.md").to_path_buf()];
|
||||
|
||||
chat.dispatch_command(SlashCommand::Status);
|
||||
|
||||
let rendered = match rx.try_recv() {
|
||||
Ok(AppEvent::InsertHistoryCell(cell)) => {
|
||||
lines_to_single_string(&cell.display_lines(/*width*/ 80))
|
||||
}
|
||||
other => panic!("expected status output, got {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
rendered.contains("Agents.md"),
|
||||
"expected /status to render app-server instruction sources, got: {rendered}"
|
||||
);
|
||||
assert!(
|
||||
!rendered.contains("Agents.md <none>"),
|
||||
"expected /status to avoid stale <none> when app-server provided instruction sources, got: {rendered}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn status_command_overlapping_refreshes_update_matching_cells_only() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -67,20 +67,10 @@ struct StatusRateLimitState {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct StatusHistoryHandle {
|
||||
agents_summary: Arc<RwLock<String>>,
|
||||
rate_limit_state: Arc<RwLock<StatusRateLimitState>>,
|
||||
}
|
||||
|
||||
impl StatusHistoryHandle {
|
||||
pub(crate) fn finish_agents_summary_discovery(&self, agents_summary: String) {
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut current = self
|
||||
.agents_summary
|
||||
.write()
|
||||
.expect("status history agents summary state poisoned");
|
||||
*current = agents_summary;
|
||||
}
|
||||
|
||||
pub(crate) fn finish_rate_limit_refresh(
|
||||
&self,
|
||||
rate_limits: &[RateLimitSnapshotDisplay],
|
||||
@@ -360,13 +350,10 @@ impl StatusHistoryCell {
|
||||
session_id,
|
||||
forked_from,
|
||||
token_usage,
|
||||
agents_summary: agents_summary.clone(),
|
||||
agents_summary,
|
||||
rate_limit_state: rate_limit_state.clone(),
|
||||
},
|
||||
StatusHistoryHandle {
|
||||
agents_summary,
|
||||
rate_limit_state,
|
||||
},
|
||||
StatusHistoryHandle { rate_limit_state },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use crate::exec_command::relativize_to_home;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::discover_project_doc_paths;
|
||||
use crate::status::StatusAccountDisplay;
|
||||
use crate::text_formatting;
|
||||
use chrono::DateTime;
|
||||
use chrono::Local;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_protocol::account::PlanType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
fn normalize_agents_display_path(path: &Path) -> String {
|
||||
@@ -36,16 +33,8 @@ pub(crate) fn compose_model_display(
|
||||
(model_name.to_string(), details)
|
||||
}
|
||||
|
||||
pub(crate) async fn discover_agents_summary(config: &Config) -> io::Result<String> {
|
||||
let paths = discover_project_doc_paths(config, LOCAL_FS.as_ref()).await?;
|
||||
Ok(compose_agents_summary(config, &paths))
|
||||
}
|
||||
|
||||
pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf]) -> String {
|
||||
pub(crate) fn compose_agents_summary(config: &Config, paths: &[PathBuf]) -> String {
|
||||
let mut rels: Vec<String> = Vec::new();
|
||||
if let Some(path) = config.user_instructions_path.as_deref() {
|
||||
rels.push(format_directory_display(path, /*max_width*/ None));
|
||||
}
|
||||
|
||||
for p in paths {
|
||||
let file_name = p
|
||||
@@ -53,14 +42,14 @@ pub(crate) fn compose_agents_summary(config: &Config, paths: &[AbsolutePathBuf])
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "<unknown>".to_string());
|
||||
let display = if let Some(parent) = p.parent() {
|
||||
if parent.as_path() == config.cwd.as_path() {
|
||||
if parent == config.cwd.as_path() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
let mut cur = config.cwd.as_path();
|
||||
let mut ups = 0usize;
|
||||
let mut reached = false;
|
||||
while let Some(c) = cur.parent() {
|
||||
if cur == parent.as_path() {
|
||||
if cur == parent {
|
||||
reached = true;
|
||||
break;
|
||||
}
|
||||
@@ -199,7 +188,6 @@ mod tests {
|
||||
use crate::legacy_core::LOCAL_PROJECT_DOC_FILENAME;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn test_config(codex_home: &TempDir, cwd: &TempDir) -> Config {
|
||||
@@ -234,52 +222,43 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_agents_summary_includes_global_agents_path() {
|
||||
async fn compose_agents_summary_includes_global_agents_path() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME);
|
||||
fs::write(&global_agents_path, "global instructions").expect("write global agents");
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
assert_eq!(
|
||||
discover_agents_summary(&config).await.expect("summary"),
|
||||
compose_agents_summary(&config, std::slice::from_ref(&global_agents_path)),
|
||||
format_directory_display(&global_agents_path, /*max_width*/ None)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_agents_summary_names_global_agents_override() {
|
||||
async fn compose_agents_summary_names_global_agents_override() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
fs::write(
|
||||
codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME),
|
||||
"global instructions",
|
||||
)
|
||||
.expect("write global agents");
|
||||
let override_path = codex_home.path().join(LOCAL_PROJECT_DOC_FILENAME);
|
||||
fs::write(&override_path, "override instructions").expect("write global override");
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
assert_eq!(
|
||||
discover_agents_summary(&config).await.expect("summary"),
|
||||
compose_agents_summary(&config, std::slice::from_ref(&override_path)),
|
||||
format_directory_display(&override_path, /*max_width*/ None)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn discover_agents_summary_orders_global_before_project_agents() {
|
||||
async fn compose_agents_summary_orders_global_before_project_agents() {
|
||||
let codex_home = TempDir::new().expect("temp codex home");
|
||||
let cwd = TempDir::new().expect("temp cwd");
|
||||
let global_agents_path = codex_home.path().join(DEFAULT_PROJECT_DOC_FILENAME);
|
||||
fs::write(&global_agents_path, "global instructions").expect("write global agents");
|
||||
fs::write(
|
||||
cwd.path().join(DEFAULT_PROJECT_DOC_FILENAME),
|
||||
"project instructions",
|
||||
)
|
||||
.expect("write project agents");
|
||||
let project_agents_path = cwd.path().join(DEFAULT_PROJECT_DOC_FILENAME);
|
||||
let config = test_config(&codex_home, &cwd).await;
|
||||
|
||||
let summary = discover_agents_summary(&config).await.expect("summary");
|
||||
let summary = compose_agents_summary(
|
||||
&config,
|
||||
&[global_agents_path.clone(), project_agents_path.clone()],
|
||||
);
|
||||
let mut paths = summary.split(", ");
|
||||
assert_eq!(
|
||||
paths.next(),
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(crate) use card::new_status_output;
|
||||
#[cfg(test)]
|
||||
pub(crate) use card::new_status_output_with_rate_limits;
|
||||
pub(crate) use card::new_status_output_with_rate_limits_handle;
|
||||
pub(crate) use helpers::discover_agents_summary;
|
||||
pub(crate) use helpers::compose_agents_summary;
|
||||
pub(crate) use helpers::format_directory_display;
|
||||
pub(crate) use helpers::format_tokens_compact;
|
||||
pub(crate) use helpers::plan_type_display_name;
|
||||
|
||||
Reference in New Issue
Block a user