Compare commits

..

4 Commits

Author SHA1 Message Date
Dylan Hurd
e332154dd4 git repo once 2025-08-06 06:24:57 -07:00
Dylan
3e8bcf0247 [prompts] Add <environment_context> (#1869)
## Summary
Includes a new user message in the api payload which provides useful
environment context for the model, so it knows about things like the
current working directory and the sandbox.

## Testing
Updated unit tests
2025-08-06 01:13:31 -07:00
Dylan
cda39e417f [tests] Investigate flakey mcp-server test (#1877)
## Summary
Have seen these tests flaking over the course of today on different
boxes. `wiremock` seems to be generally written with tokio/threads in
mind but based on the weird panics from the tests, let's see if this
helps.
2025-08-06 00:07:58 -07:00
ae
d642b07fcc [feat] add /status slash command (#1873)
- Added a `/status` command, which will be useful when we update the
home screen to print less status.
- Moved `create_config_summary_entries` to common since it's used in a
few places.
- Noticed we inconsistently had periods in slash command descriptions
and just removed them everywhere.
- Noticed the diff description was overflowing so made it shorter.
2025-08-05 23:57:52 -07:00
22 changed files with 259 additions and 123 deletions

View File

@@ -0,0 +1,29 @@
use codex_core::WireApi;
use codex_core::config::Config;
use crate::sandbox_summary::summarize_sandbox_policy;
/// Build a list of key/value pairs summarizing the effective configuration.
pub fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> {
let mut entries = vec![
("workdir", config.cwd.display().to_string()),
("model", config.model.clone()),
("provider", config.model_provider_id.clone()),
("approval", config.approval_policy.to_string()),
("sandbox", summarize_sandbox_policy(&config.sandbox_policy)),
];
if config.model_provider.wire_api == WireApi::Responses
&& config.model_family.supports_reasoning_summaries
{
entries.push((
"reasoning effort",
config.model_reasoning_effort.to_string(),
));
entries.push((
"reasoning summaries",
config.model_reasoning_summary.to_string(),
));
}
entries
}

View File

@@ -23,3 +23,7 @@ mod sandbox_summary;
#[cfg(feature = "sandbox_summary")]
pub use sandbox_summary::summarize_sandbox_policy;
mod config_summary;
pub use config_summary::create_config_summary_entries;

View File

@@ -41,11 +41,9 @@ pub(crate) async fn stream_chat_completions(
let full_instructions = prompt.get_full_instructions(model_family);
messages.push(json!({"role": "system", "content": full_instructions}));
if let Some(instr) = &prompt.get_formatted_user_instructions() {
messages.push(json!({"role": "user", "content": instr}));
}
let input = prompt.get_formatted_input();
for item in &prompt.input {
for item in &input {
match item {
ResponseItem::Message { role, content, .. } => {
let mut text = String::new();

View File

@@ -34,7 +34,6 @@ use crate::error::Result;
use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi;
use crate::models::ContentItem;
use crate::models::ResponseItem;
use crate::openai_tools::create_tools_json_for_responses_api;
use crate::protocol::TokenUsage;
@@ -146,15 +145,7 @@ impl ModelClient {
vec![]
};
let mut input_with_instructions = Vec::with_capacity(prompt.input.len() + 1);
if let Some(ui) = prompt.get_formatted_user_instructions() {
input_with_instructions.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: ui }],
});
}
input_with_instructions.extend(prompt.input.clone());
let input_with_instructions = prompt.get_formatted_input();
let payload = ResponsesApiRequest {
model: &self.config.model,

View File

@@ -1,14 +1,20 @@
use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
use crate::error::Result;
use crate::git_info::GitInfo;
use crate::model_family::ModelFamily;
use crate::models::ContentItem;
use crate::models::ResponseItem;
use crate::openai_tools::OpenAiTool;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::protocol::TokenUsage;
use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS;
use futures::Stream;
use serde::Serialize;
use std::borrow::Cow;
use std::fmt::Display;
use std::path::PathBuf;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
@@ -18,10 +24,49 @@ use tokio::sync::mpsc;
/// with this content.
const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md");
/// wraps environment context message in a tag for the model to parse more easily.
const ENVIRONMENT_CONTEXT_START: &str = "<environment_context>\n\n";
const ENVIRONMENT_CONTEXT_END: &str = "\n\n</environment_context>";
/// wraps user instructions message in a tag for the model to parse more easily.
const USER_INSTRUCTIONS_START: &str = "<user_instructions>\n\n";
const USER_INSTRUCTIONS_END: &str = "\n\n</user_instructions>";
#[derive(Debug, Clone)]
pub(crate) struct EnvironmentContext {
pub cwd: PathBuf,
pub git_info: Option<GitInfo>,
pub approval_policy: AskForApproval,
pub sandbox_policy: SandboxPolicy,
}
impl Display for EnvironmentContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"Current working directory: {}",
self.cwd.to_string_lossy()
)?;
writeln!(f, "Is directory a git repo: {}", self.git_info.is_some())?;
writeln!(f, "Approval policy: {}", self.approval_policy)?;
writeln!(f, "Sandbox policy: {}", self.sandbox_policy)?;
let network_access = match self.sandbox_policy.clone() {
SandboxPolicy::DangerFullAccess => "enabled",
SandboxPolicy::ReadOnly => "restricted",
SandboxPolicy::WorkspaceWrite { network_access, .. } => {
if network_access {
"enabled"
} else {
"restricted"
}
}
};
writeln!(f, "Network access: {network_access}")?;
Ok(())
}
}
/// API request payload for a single model turn.
#[derive(Default, Debug, Clone)]
pub struct Prompt {
@@ -33,6 +78,10 @@ pub struct Prompt {
/// Whether to store response on server side (disable_response_storage = !store).
pub store: bool,
/// A list of key-value pairs that will be added as a developer message
/// for the model to use
pub environment_context: Option<EnvironmentContext>,
/// Tools available to the model, including additional tools sourced from
/// external MCP servers.
pub tools: Vec<OpenAiTool>,
@@ -54,11 +103,37 @@ impl Prompt {
Cow::Owned(sections.join("\n"))
}
pub(crate) fn get_formatted_user_instructions(&self) -> Option<String> {
fn get_formatted_user_instructions(&self) -> Option<String> {
self.user_instructions
.as_ref()
.map(|ui| format!("{USER_INSTRUCTIONS_START}{ui}{USER_INSTRUCTIONS_END}"))
}
fn get_formatted_environment_context(&self) -> Option<String> {
self.environment_context
.as_ref()
.map(|ec| format!("{ENVIRONMENT_CONTEXT_START}{ec}{ENVIRONMENT_CONTEXT_END}"))
}
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
let mut input_with_instructions = Vec::with_capacity(self.input.len() + 2);
if let Some(ec) = self.get_formatted_environment_context() {
input_with_instructions.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: ec }],
});
}
if let Some(ui) = self.get_formatted_user_instructions() {
input_with_instructions.push(ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: ui }],
});
}
input_with_instructions.extend(self.input.clone());
input_with_instructions
}
}
#[derive(Debug)]

View File

@@ -37,6 +37,7 @@ use crate::apply_patch::convert_apply_patch_to_protocol;
use crate::apply_patch::get_writable_roots;
use crate::apply_patch::{self};
use crate::client::ModelClient;
use crate::client_common::EnvironmentContext;
use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::config::Config;
@@ -51,6 +52,8 @@ use crate::exec::SandboxType;
use crate::exec::StdoutStream;
use crate::exec::process_exec_tool_call;
use crate::exec_env::create_env;
use crate::git_info::GitInfo;
use crate::git_info::collect_git_info;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::mcp_tool_call::handle_mcp_tool_call;
use crate::models::ContentItem;
@@ -211,6 +214,7 @@ pub(crate) struct Session {
/// the model as well as sandbox policies are resolved against this path
/// instead of `std::env::current_dir()`.
pub(crate) cwd: PathBuf,
git_info: Option<GitInfo>,
base_instructions: Option<String>,
user_instructions: Option<String>,
pub(crate) approval_policy: AskForApproval,
@@ -723,11 +727,12 @@ async fn submission_loop(
}
return;
}
let git_info = collect_git_info(&cwd).await;
// Optionally resume an existing rollout.
let mut restored_items: Option<Vec<ResponseItem>> = None;
let rollout_recorder: Option<RolloutRecorder> =
if let Some(path) = resume_path.as_ref() {
match RolloutRecorder::resume(path, cwd.clone()).await {
match RolloutRecorder::resume(path, git_info.clone()).await {
Ok((rec, saved)) => {
session_id = saved.session_id;
if !saved.items.is_empty() {
@@ -747,8 +752,13 @@ async fn submission_loop(
let rollout_recorder = match rollout_recorder {
Some(rec) => Some(rec),
None => {
match RolloutRecorder::new(&config, session_id, user_instructions.clone())
.await
match RolloutRecorder::new(
&config,
session_id,
user_instructions.clone(),
git_info.clone(),
)
.await
{
Ok(r) => Some(r),
Err(e) => {
@@ -827,6 +837,7 @@ async fn submission_loop(
sandbox_policy,
shell_environment_policy: config.shell_environment_policy.clone(),
cwd,
git_info,
writable_roots,
mcp_connection_manager,
notify,
@@ -1224,6 +1235,12 @@ async fn run_turn(
store: !sess.disable_response_storage,
tools,
base_instructions_override: sess.base_instructions.clone(),
environment_context: Some(EnvironmentContext {
cwd: sess.cwd.clone(),
git_info: sess.git_info.clone(),
approval_policy: sess.approval_policy,
sandbox_policy: sess.sandbox_policy.clone(),
}),
};
let mut retries = 0;
@@ -1449,6 +1466,7 @@ async fn run_compact_task(
input: turn_input,
user_instructions: None,
store: !sess.disable_response_storage,
environment_context: None,
tools: Vec::new(),
base_instructions_override: Some(compact_instructions.clone()),
};

View File

@@ -9,7 +9,7 @@ use tokio::time::timeout;
/// Timeout for git commands to prevent freezing on large repositories
const GIT_COMMAND_TIMEOUT: TokioDuration = TokioDuration::from_secs(5);
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GitInfo {
/// Current commit hash (SHA)
#[serde(skip_serializing_if = "Option::is_none")]

View File

@@ -15,7 +15,7 @@ pub use codex::Codex;
pub use codex::CodexSpawnOk;
pub mod codex_wrapper;
pub mod config;
mod config_profile;
pub mod config_profile;
pub mod config_types;
mod conversation_history;
pub mod error;
@@ -33,7 +33,7 @@ pub use model_provider_info::ModelProviderInfo;
pub use model_provider_info::WireApi;
pub use model_provider_info::built_in_model_providers;
pub use model_provider_info::create_oss_provider_with_base_url;
mod model_family;
pub mod model_family;
mod models;
mod openai_model_info;
mod openai_tools;
@@ -43,9 +43,9 @@ pub mod protocol;
mod rollout;
pub(crate) mod safety;
pub mod seatbelt;
mod shell;
pub mod shell;
pub mod spawn;
mod turn_diff_tracker;
pub mod turn_diff_tracker;
mod user_notification;
pub mod util;
pub use apply_patch::CODEX_APPLY_PATCH_ARG1;

View File

@@ -159,7 +159,8 @@ pub enum AskForApproval {
}
/// Determines execution restrictions for model shell commands.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display)]
#[strum(serialize_all = "kebab-case")]
#[serde(tag = "mode", rename_all = "kebab-case")]
pub enum SandboxPolicy {
/// No restrictions whatsoever. Use with caution.

View File

@@ -21,7 +21,6 @@ use uuid::Uuid;
use crate::config::Config;
use crate::git_info::GitInfo;
use crate::git_info::collect_git_info;
use crate::models::ResponseItem;
const SESSIONS_SUBDIR: &str = "sessions";
@@ -82,6 +81,7 @@ impl RolloutRecorder {
config: &Config,
uuid: Uuid,
instructions: Option<String>,
git_info: Option<GitInfo>,
) -> std::io::Result<Self> {
let LogFileInfo {
file,
@@ -96,9 +96,6 @@ impl RolloutRecorder {
.format(timestamp_format)
.map_err(|e| IoError::other(format!("failed to format timestamp: {e}")))?;
// Clone the cwd for the spawned task to collect git info asynchronously
let cwd = config.cwd.clone();
// A reasonably-sized bounded channel. If the buffer fills up the send
// future will yield, which is fine we only need to ensure we do not
// perform *blocking* I/O on the caller's thread.
@@ -115,7 +112,7 @@ impl RolloutRecorder {
id: session_id,
instructions,
}),
cwd,
git_info,
));
Ok(Self { tx })
@@ -157,7 +154,7 @@ impl RolloutRecorder {
pub async fn resume(
path: &Path,
cwd: std::path::PathBuf,
git_info: Option<GitInfo>,
) -> std::io::Result<(Self, SavedSession)> {
info!("Resuming rollout from {path:?}");
let text = tokio::fs::read_to_string(path).await?;
@@ -220,7 +217,7 @@ impl RolloutRecorder {
tokio::fs::File::from_std(file),
rx,
None,
cwd,
git_info,
));
info!("Resumed rollout successfully from {path:?}");
Ok((Self { tx }, saved))
@@ -291,13 +288,12 @@ async fn rollout_writer(
file: tokio::fs::File,
mut rx: mpsc::Receiver<RolloutCmd>,
mut meta: Option<SessionMeta>,
cwd: std::path::PathBuf,
git_info: Option<GitInfo>,
) -> std::io::Result<()> {
let mut writer = JsonlWriter { file };
// If we have a meta, collect git info asynchronously and write meta first
if let Some(session_meta) = meta.take() {
let git_info = collect_git_info(&cwd).await;
let session_meta_with_git = SessionMetaWithGit {
meta: session_meta,
git: git_info,

View File

@@ -1,13 +1,13 @@
#[cfg(target_os = "macos")]
use shlex;
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct ZshShell {
pub struct ZshShell {
shell_path: String,
zshrc_path: String,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Shell {
#[cfg(target_os = "macos")]
pub enum Shell {
Zsh(ZshShell),
Unknown,
}
@@ -15,7 +15,6 @@ pub(crate) enum Shell {
impl Shell {
pub fn format_default_shell_invocation(&self, command: Vec<String>) -> Option<Vec<String>> {
match self {
#[cfg(target_os = "macos")]
Shell::Zsh(zsh) => {
if !std::path::Path::new(&zsh.zshrc_path).exists() {
return None;

View File

@@ -1,3 +1,5 @@
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use std::path::PathBuf;
use chrono::Utc;
@@ -32,6 +34,32 @@ fn sse_completed(id: &str) -> String {
load_sse_fixture_with_id("tests/fixtures/completed_template.json", id)
}
fn assert_message_role(request_body: &serde_json::Value, role: &str) {
assert_eq!(request_body["role"].as_str().unwrap(), role);
}
fn assert_message_starts_with(request_body: &serde_json::Value, text: &str) {
let content = request_body["content"][0]["text"]
.as_str()
.expect("invalid message content");
assert!(
content.starts_with(text),
"expected message content '{content}' to start with '{text}'"
);
}
fn assert_message_ends_with(request_body: &serde_json::Value, text: &str) {
let content = request_body["content"][0]["text"]
.as_str()
.expect("invalid message content");
assert!(
content.ends_with(text),
"expected message content '{content}' to end with '{text}'"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn includes_session_id_and_model_headers_in_request() {
#![allow(clippy::unwrap_used)]
@@ -371,19 +399,12 @@ async fn includes_user_instructions_message_in_request() {
.unwrap()
.contains("be nice")
);
assert_eq!(request_body["input"][0]["role"], "user");
assert!(
request_body["input"][0]["content"][0]["text"]
.as_str()
.unwrap()
.starts_with("<user_instructions>\n\nbe nice")
);
assert!(
request_body["input"][0]["content"][0]["text"]
.as_str()
.unwrap()
.ends_with("</user_instructions>")
);
assert_message_role(&request_body["input"][0], "user");
assert_message_starts_with(&request_body["input"][0], "<environment_context>\n\n");
assert_message_ends_with(&request_body["input"][0], "</environment_context>");
assert_message_role(&request_body["input"][1], "user");
assert_message_starts_with(&request_body["input"][1], "<user_instructions>\n\n");
assert_message_ends_with(&request_body["input"][1], "</user_instructions>");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -1,7 +1,5 @@
use std::path::Path;
use codex_common::summarize_sandbox_policy;
use codex_core::WireApi;
use codex_core::config::Config;
use codex_core::protocol::Event;
@@ -19,30 +17,6 @@ pub(crate) trait EventProcessor {
fn process_event(&mut self, event: Event) -> CodexStatus;
}
pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> {
let mut entries = vec![
("workdir", config.cwd.display().to_string()),
("model", config.model.clone()),
("provider", config.model_provider_id.clone()),
("approval", config.approval_policy.to_string()),
("sandbox", summarize_sandbox_policy(&config.sandbox_policy)),
];
if config.model_provider.wire_api == WireApi::Responses
&& config.model_family.supports_reasoning_summaries
{
entries.push((
"reasoning effort",
config.model_reasoning_effort.to_string(),
));
entries.push((
"reasoning summaries",
config.model_reasoning_summary.to_string(),
));
}
entries
}
pub(crate) fn handle_last_message(last_agent_message: Option<&str>, output_file: &Path) {
let message = last_agent_message.unwrap_or_default();
write_last_message_file(message, Some(output_file));

View File

@@ -33,8 +33,8 @@ use std::time::Instant;
use crate::event_processor::CodexStatus;
use crate::event_processor::EventProcessor;
use crate::event_processor::create_config_summary_entries;
use crate::event_processor::handle_last_message;
use codex_common::create_config_summary_entries;
/// This should be configurable. When used in CI, users may not want to impose
/// a limit so they can see the full transcript.

View File

@@ -9,8 +9,8 @@ use serde_json::json;
use crate::event_processor::CodexStatus;
use crate::event_processor::EventProcessor;
use crate::event_processor::create_config_summary_entries;
use crate::event_processor::handle_last_message;
use codex_common::create_config_summary_entries;
pub(crate) struct EventProcessorWithJsonOutput {
last_message_path: Option<PathBuf>,

View File

@@ -18,7 +18,7 @@ use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn test_send_message_success() {
// Spin up a mock completions server that immediately ends the Codex turn.
// Two Codex turns hit the mock model (session start + send-user-message). Provide two SSE responses.
@@ -99,13 +99,13 @@ async fn test_send_message_success() {
response
);
// wait for the server to hear the user message
sleep(Duration::from_secs(1));
sleep(Duration::from_secs(10));
// Ensure the server and tempdir live until end of test
drop(server);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[tokio::test]
async fn test_send_message_session_not_found() {
// Start MCP without creating a Codex session
let codex_home = TempDir::new().expect("tempdir");

View File

@@ -330,6 +330,11 @@ impl App<'_> {
widget.add_diff_output(text);
}
}
SlashCommand::Status => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.add_status_output();
}
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use std::collections::HashMap;

View File

@@ -522,6 +522,13 @@ impl ChatWidget<'_> {
self.add_to_history(HistoryCell::new_diff_output(diff_output.clone()));
}
pub(crate) fn add_status_output(&mut self) {
self.add_to_history(HistoryCell::new_status_output(
&self.config,
&self.token_usage,
));
}
/// Forward file-search results to the bottom pane.
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
self.bottom_pane.on_file_search_result(query, matches);

View File

@@ -3,9 +3,8 @@ use crate::text_block::TextBlock;
use crate::text_formatting::format_and_truncate_tool_result;
use base64::Engine;
use codex_ansi_escape::ansi_escape_line;
use codex_common::create_config_summary_entries;
use codex_common::elapsed::format_duration;
use codex_common::summarize_sandbox_policy;
use codex_core::WireApi;
use codex_core::config::Config;
use codex_core::plan_tool::PlanItemArg;
use codex_core::plan_tool::StepStatus;
@@ -14,6 +13,7 @@ use codex_core::protocol::FileChange;
use codex_core::protocol::McpInvocation;
use codex_core::protocol::PatchApplyEndEvent;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::protocol::TokenUsage;
use image::DynamicImage;
use image::ImageReader;
use mcp_types::EmbeddedResourceResource;
@@ -114,6 +114,11 @@ pub(crate) enum HistoryCell {
view: TextBlock,
},
/// Output from the `/status` command.
StatusOutput {
view: TextBlock,
},
/// Error event from the backend.
ErrorEvent {
view: TextBlock,
@@ -154,6 +159,7 @@ impl HistoryCell {
| HistoryCell::UserPrompt { view }
| HistoryCell::BackgroundEvent { view }
| HistoryCell::GitDiffOutput { view }
| HistoryCell::StatusOutput { view }
| HistoryCell::ErrorEvent { view }
| HistoryCell::SessionInfo { view }
| HistoryCell::CompletedExecCommand { view }
@@ -200,26 +206,7 @@ impl HistoryCell {
]),
];
let mut entries = vec![
("workdir", config.cwd.display().to_string()),
("model", config.model.clone()),
("provider", config.model_provider_id.clone()),
("approval", config.approval_policy.to_string()),
("sandbox", summarize_sandbox_policy(&config.sandbox_policy)),
];
if config.model_provider.wire_api == WireApi::Responses
&& config.model_family.supports_reasoning_summaries
{
entries.push((
"reasoning effort",
config.model_reasoning_effort.to_string(),
));
entries.push((
"reasoning summaries",
config.model_reasoning_summary.to_string(),
));
}
for (key, value) in entries {
for (key, value) in create_config_summary_entries(config) {
lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()]));
}
lines.push(Line::from(""));
@@ -476,6 +463,49 @@ impl HistoryCell {
}
}
pub(crate) fn new_status_output(config: &Config, usage: &TokenUsage) -> Self {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from("/status".magenta()));
// Config
for (key, value) in create_config_summary_entries(config) {
lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()]));
}
// Token usage
lines.push(Line::from(""));
lines.push(Line::from("token usage".bold()));
lines.push(Line::from(vec![
" input: ".bold(),
usage.input_tokens.to_string().into(),
]));
lines.push(Line::from(vec![
" cached input: ".bold(),
usage.cached_input_tokens.unwrap_or(0).to_string().into(),
]));
lines.push(Line::from(vec![
" output: ".bold(),
usage.output_tokens.to_string().into(),
]));
lines.push(Line::from(vec![
" reasoning output: ".bold(),
usage
.reasoning_output_tokens
.unwrap_or(0)
.to_string()
.into(),
]));
lines.push(Line::from(vec![
" total: ".bold(),
usage.total_tokens.to_string().into(),
]));
lines.push(Line::from(""));
HistoryCell::StatusOutput {
view: TextBlock::new(lines),
}
}
pub(crate) fn new_error_event(message: String) -> Self {
let lines: Vec<Line<'static>> = vec![
vec!["ERROR: ".red().bold(), message.into()].into(),

View File

@@ -27,23 +27,14 @@ mod bottom_pane;
mod chatwidget;
mod citation_regex;
mod cli;
#[cfg(feature = "vt100-tests")]
pub mod custom_terminal;
#[cfg(not(feature = "vt100-tests"))]
mod custom_terminal;
mod exec_command;
mod file_search;
mod get_git_diff;
mod git_warning_screen;
mod history_cell;
#[cfg(feature = "vt100-tests")]
pub mod insert_history;
#[cfg(not(feature = "vt100-tests"))]
mod insert_history;
#[cfg(feature = "vt100-tests")]
pub mod live_wrap;
#[cfg(not(feature = "vt100-tests"))]
mod live_wrap;
mod log_layer;
mod markdown;
mod slash_command;

View File

@@ -1,4 +1,5 @@
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
/// A single visual row produced by RowBuilder.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -9,9 +10,7 @@ pub struct Row {
}
impl Row {
#[cfg(test)]
pub fn width(&self) -> usize {
use unicode_width::UnicodeWidthStr;
self.text.width()
}
}
@@ -40,7 +39,6 @@ impl RowBuilder {
self.target_width
}
#[cfg(test)]
pub fn set_width(&mut self, width: usize) {
self.target_width = width.max(1);
// Rewrap everything we have (simple approach for Step 1).
@@ -89,7 +87,6 @@ impl RowBuilder {
}
/// Return a snapshot of produced rows (non-draining).
#[cfg(test)]
pub fn rows(&self) -> &[Row] {
&self.rows
}

View File

@@ -15,6 +15,7 @@ pub enum SlashCommand {
New,
Compact,
Diff,
Status,
Quit,
#[cfg(debug_assertions)]
TestApproval,
@@ -24,12 +25,11 @@ impl SlashCommand {
/// User-visible description shown in the popup.
pub fn description(self) -> &'static str {
match self {
SlashCommand::New => "Start a new chat.",
SlashCommand::Compact => "Compact the chat history.",
SlashCommand::Quit => "Exit the application.",
SlashCommand::Diff => {
"Show git diff of the working directory (including untracked files)"
}
SlashCommand::New => "Start a new chat",
SlashCommand::Compact => "Compact the chat history",
SlashCommand::Quit => "Exit the application",
SlashCommand::Diff => "Show git diff (including untracked files)",
SlashCommand::Status => "Show current session configuration and token usage",
#[cfg(debug_assertions)]
SlashCommand::TestApproval => "Test approval request",
}