mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
c884536d84
## Why [#25345](https://github.com/openai/codex/pull/25345) was approved, green, and squash-merged into its stacked base branch, `fcoury/tokenmaxxing-api`. Four minutes later, that base branch was force-pushed back to an API-only rebased head while preparing [#25344](https://github.com/openai/codex/pull/25344) for `main`. As a result, the squash commit from #25345 was orphaned and the TUI command never reached `main` or a release. This PR relands the orphaned TUI change from [`411410b8`](https://github.com/openai/codex/commit/411410b85c2d8eb050d441f17396c5c4048d866f) on current `main`. ## What changed - Add `/usage`, `/usage daily`, `/usage weekly`, and `/usage cumulative` for account token activity. - Fetch account usage asynchronously through the existing `account/usage/read` app-server RPC. - Render daily, weekly, and cumulative activity with theme-aware terminal palettes and bounded transient cards. - Preserve transcript ordering while assistant streams, history consolidations, active cells, and hooks complete. - Hide `/usage` from completion when backend auth is unavailable while keeping typed-command guidance. - Carry current-main behavior forward for cwd-aware Markdown parsing, Windows Terminal color detection, and personal access token auth. - Clear pending usage cards on thread rollback and delay completed cards until live hook output is committed. - Add focused regression and snapshot coverage for loading, auth errors, invalid views, rollback, hook ordering, layout, and charts. ## Prior review The original implementation was approved by Eric Traut in #25345 after testing multiple themes and light/dark terminals. This PR preserves that reviewed implementation while adapting it to current `main` and adding regression coverage for newer rollback and hook lifecycle behavior. ## Validation - `just test -p codex-tui token_activity palette renderable usage_command` — 37 passed. - Focused rollback, hook-ordering, and error snapshot tests — 4 passed. - `just fix -p codex-tui` — passed. - `UV_CACHE_DIR=/private/tmp/codex-uv-cache just fmt` — passed. - `cargo insta pending-snapshots` — no pending snapshots. - `just test -p codex-tui` — 2,870 passed; two unrelated guardian feature-flag tests failed because their expected `OverrideTurnContext` event was absent: - `update_feature_flags_disabling_guardian_clears_manual_review_policy_without_history` - `update_feature_flags_disabling_guardian_clears_review_policy_and_restores_default` - `just argument-comment-lint` could not complete because the local Bazel LLVM `compiler-rt` repository is missing `include/sanitizer/*.h`. The touched Rust diff was manually inspected and no missing opaque-literal argument comments were found.
309 lines
11 KiB
Rust
309 lines
11 KiB
Rust
use strum::IntoEnumIterator;
|
|
use strum_macros::AsRefStr;
|
|
use strum_macros::EnumIter;
|
|
use strum_macros::EnumString;
|
|
use strum_macros::IntoStaticStr;
|
|
|
|
/// Commands that can be invoked by starting a message with a leading slash.
|
|
#[derive(
|
|
Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString, EnumIter, AsRefStr, IntoStaticStr,
|
|
)]
|
|
#[strum(serialize_all = "kebab-case")]
|
|
pub enum SlashCommand {
|
|
// DO NOT ALPHA-SORT! Enum order is presentation order in the popup, so
|
|
// more frequently used commands should be listed first.
|
|
Model,
|
|
Ide,
|
|
Permissions,
|
|
Keymap,
|
|
Vim,
|
|
#[strum(serialize = "setup-default-sandbox")]
|
|
ElevateSandbox,
|
|
#[strum(serialize = "sandbox-add-read-dir")]
|
|
SandboxReadRoot,
|
|
Experimental,
|
|
#[strum(to_string = "approve")]
|
|
AutoReview,
|
|
Memories,
|
|
Skills,
|
|
Import,
|
|
Hooks,
|
|
Review,
|
|
Rename,
|
|
New,
|
|
Archive,
|
|
Delete,
|
|
Resume,
|
|
Fork,
|
|
App,
|
|
Init,
|
|
Compact,
|
|
Plan,
|
|
Goal,
|
|
Agent,
|
|
Side,
|
|
Btw,
|
|
Copy,
|
|
Raw,
|
|
Diff,
|
|
Mention,
|
|
Status,
|
|
Usage,
|
|
DebugConfig,
|
|
Title,
|
|
Statusline,
|
|
Theme,
|
|
#[strum(to_string = "pets", serialize = "pet")]
|
|
Pets,
|
|
Mcp,
|
|
Apps,
|
|
Plugins,
|
|
Logout,
|
|
Quit,
|
|
Exit,
|
|
Feedback,
|
|
Rollout,
|
|
Ps,
|
|
#[strum(to_string = "stop", serialize = "clean")]
|
|
Stop,
|
|
Clear,
|
|
Personality,
|
|
TestApproval,
|
|
#[strum(serialize = "subagents")]
|
|
MultiAgents,
|
|
// Debugging commands.
|
|
#[strum(serialize = "debug-m-drop")]
|
|
MemoryDrop,
|
|
#[strum(serialize = "debug-m-update")]
|
|
MemoryUpdate,
|
|
}
|
|
|
|
impl SlashCommand {
|
|
/// User-visible description shown in the popup.
|
|
pub fn description(self) -> &'static str {
|
|
match self {
|
|
SlashCommand::Feedback => "send logs to maintainers",
|
|
SlashCommand::New => "start a new chat during a conversation",
|
|
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
|
|
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
|
SlashCommand::Review => "review my current changes and find issues",
|
|
SlashCommand::Rename => "rename the current thread",
|
|
SlashCommand::Resume => "resume a saved chat",
|
|
SlashCommand::Archive => "archive this session and exit",
|
|
SlashCommand::Delete => "permanently delete this session and exit",
|
|
SlashCommand::Clear => "clear the terminal and start a new chat",
|
|
SlashCommand::Fork => "fork the current chat",
|
|
SlashCommand::App => "continue this session in Codex Desktop",
|
|
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
|
|
SlashCommand::Copy => "copy last response as markdown",
|
|
SlashCommand::Raw => "toggle raw scrollback mode for copy-friendly terminal selection",
|
|
SlashCommand::Diff => "show git diff (including untracked files)",
|
|
SlashCommand::Mention => "mention a file",
|
|
SlashCommand::Skills => "use skills to improve how Codex performs specific tasks",
|
|
SlashCommand::Import => "import setup, this project, and recent chats from Claude Code",
|
|
SlashCommand::Hooks => "view and manage lifecycle hooks",
|
|
SlashCommand::Status => "show current session configuration and token usage",
|
|
SlashCommand::Usage => "show account usage activity",
|
|
SlashCommand::DebugConfig => "show config layers and requirement sources for debugging",
|
|
SlashCommand::Title => "configure which items appear in the terminal title",
|
|
SlashCommand::Statusline => "configure which items appear in the status line",
|
|
SlashCommand::Theme => "choose a syntax highlighting theme",
|
|
SlashCommand::Pets => "choose or hide the terminal pet",
|
|
SlashCommand::Ps => "list background terminals",
|
|
SlashCommand::Stop => "stop all background terminals",
|
|
SlashCommand::MemoryDrop => "DO NOT USE",
|
|
SlashCommand::MemoryUpdate => "DO NOT USE",
|
|
SlashCommand::Model => "choose what model and reasoning effort to use",
|
|
SlashCommand::Ide => {
|
|
"include current selection, open files, and other context from your IDE"
|
|
}
|
|
SlashCommand::Personality => "choose a communication style for Codex",
|
|
SlashCommand::Plan => "switch to Plan mode",
|
|
SlashCommand::Goal => "set or view the goal for a long-running task",
|
|
SlashCommand::Agent | SlashCommand::MultiAgents => "switch the active agent thread",
|
|
SlashCommand::Side | SlashCommand::Btw => {
|
|
"start a side conversation in an ephemeral fork"
|
|
}
|
|
SlashCommand::Permissions => "choose what Codex is allowed to do",
|
|
SlashCommand::Keymap => "remap TUI shortcuts",
|
|
SlashCommand::Vim => "toggle Vim mode for the composer",
|
|
SlashCommand::ElevateSandbox => "set up elevated agent sandbox",
|
|
SlashCommand::SandboxReadRoot => {
|
|
"let sandbox read a directory: /sandbox-add-read-dir <absolute_path>"
|
|
}
|
|
SlashCommand::Experimental => "toggle experimental features",
|
|
SlashCommand::AutoReview => "approve one retry of a recent auto-review denial",
|
|
SlashCommand::Memories => "configure memory use and generation",
|
|
SlashCommand::Mcp => "list configured MCP tools; use /mcp verbose for details",
|
|
SlashCommand::Apps => "manage apps",
|
|
SlashCommand::Plugins => "browse plugins",
|
|
SlashCommand::Logout => "log out of Codex",
|
|
SlashCommand::Rollout => "print the rollout file path",
|
|
SlashCommand::TestApproval => "test approval request",
|
|
}
|
|
}
|
|
|
|
/// Command string without the leading '/'. Provided for compatibility with
|
|
/// existing code that expects a method named `command()`.
|
|
pub fn command(self) -> &'static str {
|
|
self.into()
|
|
}
|
|
|
|
/// Whether this command supports inline args (for example `/review ...`).
|
|
pub fn supports_inline_args(self) -> bool {
|
|
matches!(
|
|
self,
|
|
SlashCommand::Review
|
|
| SlashCommand::Rename
|
|
| SlashCommand::Plan
|
|
| SlashCommand::Goal
|
|
| SlashCommand::Ide
|
|
| SlashCommand::Keymap
|
|
| SlashCommand::Mcp
|
|
| SlashCommand::Raw
|
|
| SlashCommand::Usage
|
|
| SlashCommand::Pets
|
|
| SlashCommand::Side
|
|
| SlashCommand::Btw
|
|
| SlashCommand::Resume
|
|
| SlashCommand::SandboxReadRoot
|
|
)
|
|
}
|
|
|
|
/// Whether this command remains available inside an active side conversation.
|
|
pub fn available_in_side_conversation(self) -> bool {
|
|
matches!(
|
|
self,
|
|
SlashCommand::Copy
|
|
| SlashCommand::Raw
|
|
| SlashCommand::Diff
|
|
| SlashCommand::Mention
|
|
| SlashCommand::Status
|
|
| SlashCommand::Usage
|
|
| SlashCommand::Ide
|
|
)
|
|
}
|
|
|
|
/// Whether this command can be run while a task is in progress.
|
|
pub fn available_during_task(self) -> bool {
|
|
match self {
|
|
SlashCommand::New
|
|
| SlashCommand::Archive
|
|
| SlashCommand::Delete
|
|
| SlashCommand::Resume
|
|
| SlashCommand::Fork
|
|
| SlashCommand::Init
|
|
| SlashCommand::Compact
|
|
| SlashCommand::Model
|
|
| SlashCommand::Personality
|
|
| SlashCommand::Permissions
|
|
| SlashCommand::Keymap
|
|
| SlashCommand::Vim
|
|
| SlashCommand::ElevateSandbox
|
|
| SlashCommand::SandboxReadRoot
|
|
| SlashCommand::Experimental
|
|
| SlashCommand::Memories
|
|
| SlashCommand::Import
|
|
| SlashCommand::Review
|
|
| SlashCommand::Plan
|
|
| SlashCommand::Clear
|
|
| SlashCommand::Logout
|
|
| SlashCommand::MemoryDrop
|
|
| SlashCommand::MemoryUpdate => false,
|
|
SlashCommand::Diff
|
|
| SlashCommand::Copy
|
|
| SlashCommand::Raw
|
|
| SlashCommand::Rename
|
|
| SlashCommand::Mention
|
|
| SlashCommand::Skills
|
|
| SlashCommand::Hooks
|
|
| SlashCommand::Status
|
|
| SlashCommand::Usage
|
|
| SlashCommand::DebugConfig
|
|
| SlashCommand::Ps
|
|
| SlashCommand::Stop
|
|
| SlashCommand::App
|
|
| SlashCommand::Goal
|
|
| SlashCommand::Mcp
|
|
| SlashCommand::Apps
|
|
| SlashCommand::Plugins
|
|
| SlashCommand::Title
|
|
| SlashCommand::Statusline
|
|
| SlashCommand::AutoReview
|
|
| SlashCommand::Feedback
|
|
| SlashCommand::Ide
|
|
| SlashCommand::Quit
|
|
| SlashCommand::Exit
|
|
| SlashCommand::Side
|
|
| SlashCommand::Btw => true,
|
|
SlashCommand::Rollout => true,
|
|
SlashCommand::TestApproval => true,
|
|
SlashCommand::Agent | SlashCommand::MultiAgents => true,
|
|
SlashCommand::Theme | SlashCommand::Pets => false,
|
|
}
|
|
}
|
|
|
|
fn is_visible(self) -> bool {
|
|
match self {
|
|
SlashCommand::SandboxReadRoot => cfg!(target_os = "windows"),
|
|
SlashCommand::Copy => !cfg!(target_os = "android"),
|
|
SlashCommand::App => cfg!(any(target_os = "macos", target_os = "windows")),
|
|
SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions),
|
|
_ => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return all built-in commands in a Vec paired with their command string.
|
|
pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
|
|
SlashCommand::iter()
|
|
.filter(|command| command.is_visible())
|
|
.map(|c| (c.command(), c))
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
use std::str::FromStr;
|
|
|
|
use super::SlashCommand;
|
|
|
|
#[test]
|
|
fn stop_command_is_canonical_name() {
|
|
assert_eq!(SlashCommand::Stop.command(), "stop");
|
|
}
|
|
|
|
#[test]
|
|
fn clean_alias_parses_to_stop_command() {
|
|
assert_eq!(SlashCommand::from_str("clean"), Ok(SlashCommand::Stop));
|
|
}
|
|
|
|
#[test]
|
|
fn pet_alias_parses_to_pets_command() {
|
|
assert_eq!(SlashCommand::Pets.command(), "pets");
|
|
assert_eq!(SlashCommand::from_str("pet"), Ok(SlashCommand::Pets));
|
|
}
|
|
|
|
#[test]
|
|
fn certain_commands_are_available_during_task() {
|
|
assert!(SlashCommand::Goal.available_during_task());
|
|
assert!(SlashCommand::Ide.available_during_task());
|
|
assert!(SlashCommand::Title.available_during_task());
|
|
assert!(SlashCommand::Statusline.available_during_task());
|
|
assert!(SlashCommand::Raw.available_during_task());
|
|
assert!(SlashCommand::Raw.available_in_side_conversation());
|
|
assert!(SlashCommand::Raw.supports_inline_args());
|
|
assert!(SlashCommand::App.available_during_task());
|
|
}
|
|
|
|
#[test]
|
|
fn auto_review_command_is_approve() {
|
|
assert_eq!(SlashCommand::AutoReview.command(), "approve");
|
|
assert_eq!(
|
|
SlashCommand::from_str("approve"),
|
|
Ok(SlashCommand::AutoReview)
|
|
);
|
|
}
|
|
}
|