mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): redesign session picker (#20065)
## Why The resume/fork picker is becoming the main way users recover previous work, but the old fixed table made sessions hard to scan once thread names, branches, working directories, and timestamps all mattered. This redesign makes the picker denser by default, easier to search, and safer to inspect before resuming or forking. <table> <tr> <td> <img width="1660" height="1103" alt="CleanShot 2026-05-03 at 12 34 10" src="https://github.com/user-attachments/assets/313ede1d-1da4-4863-acd2-56b3e27e9703" /> </td> <td> <img width="1662" height="1100" alt="CleanShot 2026-05-03 at 12 34 15" src="https://github.com/user-attachments/assets/cfde7d5c-bab0-4994-a807-254e53f344ea" /> </td> </tr> <tr> <td> <img width="1664" height="1107" alt="CleanShot 2026-05-03 at 12 39 22" src="https://github.com/user-attachments/assets/e1ee58ca-4dc5-4a35-ae0f-47562da3974c" /> </td> <td> <img width="1662" height="1100" alt="CleanShot 2026-05-03 at 12 35 09" src="https://github.com/user-attachments/assets/9c888072-eedf-4f45-985c-0c14df28bcc7" /> </td> </tr> </table> ## What Changed - Replaces the old session table with responsive session rows that prioritize the session name or preview, then show timestamp, cwd, and branch metadata. - Makes dense view the default while keeping comfortable view available through `Ctrl+O`. - Persists the picker view preference in `[tui].session_picker_view`, including active profile-scoped config. - Adds sort/filter controls for updated time, created time, cwd, and all sessions. - Expands search matching across session name, preview, thread id, branch, and cwd. - Makes `Esc` safer in search mode: it clears an active query before starting a new session. - Adds lazy transcript inspection: - `Space` expands recent transcript context inline. - `Ctrl+T` opens a transcript overlay. - raw reasoning visibility follows `show_raw_agent_reasoning`. - Keeps remote cwd filtering server-side for remote app-server sessions so local path normalization does not incorrectly hide remote results. - Updates snapshots and config schema for the new picker states and config option. ## How to Test 1. Start Codex in a repo with several saved sessions. 2. Press `Ctrl+R` / resume picker entry point. 3. Confirm the picker opens in dense mode and shows session name or preview, timestamp, cwd, and branch metadata. 4. Press `Ctrl+O` and confirm it switches between dense and comfortable views. 5. Restart Codex and confirm the selected view persists. 6. Type a query that matches a branch, cwd, thread id, or session name; confirm matching sessions appear. 7. Press `Esc` while the query is non-empty and confirm it clears search instead of starting a new session. 8. Select a session and press `Space`; confirm recent transcript context expands inline. 9. Press `Ctrl+T`; confirm the transcript overlay opens and respects raw-reasoning visibility settings. Targeted tests: - `cargo test -p codex-tui resume_picker --no-fail-fast` - `cargo test -p codex-core runtime_config_resolves_session_picker_view_default_and_override` - `cargo test -p codex-core profile_tui_rejects_unsupported_settings` - `cargo check -p codex-thread-manager-sample` - `cargo insta pending-snapshots`
This commit is contained in:
committed by
GitHub
Unverified
parent
52fbbe7cdd
commit
3b2ebb368e
@@ -7,6 +7,7 @@ use crate::config_toml::ToolsToml;
|
||||
use crate::types::AnalyticsConfigToml;
|
||||
use crate::types::ApprovalsReviewer;
|
||||
use crate::types::Personality;
|
||||
use crate::types::SessionPickerViewMode;
|
||||
use crate::types::WindowsToml;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
@@ -63,6 +64,9 @@ pub struct ConfigProfile {
|
||||
pub tools: Option<ToolsToml>,
|
||||
pub web_search: Option<WebSearchMode>,
|
||||
pub analytics: Option<AnalyticsConfigToml>,
|
||||
/// TUI settings scoped to this profile.
|
||||
#[serde(default)]
|
||||
pub tui: Option<ProfileTui>,
|
||||
#[serde(default)]
|
||||
pub windows: Option<WindowsToml>,
|
||||
/// Optional feature toggles scoped to this profile.
|
||||
@@ -73,6 +77,16 @@ pub struct ConfigProfile {
|
||||
pub oss_provider: Option<String>,
|
||||
}
|
||||
|
||||
/// TUI settings supported inside a named profile.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ProfileTui {
|
||||
/// Preferred layout for resume/fork session picker results.
|
||||
#[serde(default)]
|
||||
pub session_picker_view: Option<SessionPickerViewMode>,
|
||||
}
|
||||
|
||||
impl From<ConfigProfile> for codex_app_server_protocol::Profile {
|
||||
fn from(config_profile: ConfigProfile) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -57,6 +57,30 @@ const fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Preferred layout for the resume/fork session picker.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Copy, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SessionPickerViewMode {
|
||||
Comfortable,
|
||||
#[default]
|
||||
Dense,
|
||||
}
|
||||
|
||||
impl SessionPickerViewMode {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Comfortable => "comfortable",
|
||||
Self::Dense => "dense",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionPickerViewMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine where Codex should store CLI auth credentials.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -661,6 +685,10 @@ pub struct Tui {
|
||||
#[serde(default)]
|
||||
pub theme: Option<String>,
|
||||
|
||||
/// Preferred layout for resume/fork session picker results.
|
||||
#[serde(default)]
|
||||
pub session_picker_view: Option<SessionPickerViewMode>,
|
||||
|
||||
/// Keybinding overrides for the TUI.
|
||||
///
|
||||
/// This supports rebinding selected actions globally and by context.
|
||||
|
||||
@@ -18,6 +18,7 @@ pub use codex_config::types::ModelAvailabilityNuxConfig;
|
||||
pub use codex_config::types::Notice;
|
||||
pub use codex_config::types::OAuthCredentialsStoreMode;
|
||||
pub use codex_config::types::OtelConfig;
|
||||
pub use codex_config::types::SessionPickerViewMode;
|
||||
pub use codex_config::types::ToolSuggestConfig;
|
||||
pub use codex_config::types::TuiKeymap;
|
||||
pub use codex_config::types::TuiNotificationSettings;
|
||||
|
||||
@@ -674,6 +674,15 @@
|
||||
"tools_view_image": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tui": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ProfileTui"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "TUI settings scoped to this profile."
|
||||
},
|
||||
"web_search": {
|
||||
"$ref": "#/definitions/WebSearchMode"
|
||||
},
|
||||
@@ -1876,6 +1885,22 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProfileTui": {
|
||||
"additionalProperties": false,
|
||||
"description": "TUI settings supported inside a named profile.",
|
||||
"properties": {
|
||||
"session_picker_view": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionPickerViewMode"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Preferred layout for resume/fork session picker results."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ProjectConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -2163,6 +2188,14 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionPickerViewMode": {
|
||||
"description": "Preferred layout for the resume/fork session picker.",
|
||||
"enum": [
|
||||
"comfortable",
|
||||
"dense"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ShellEnvironmentPolicyInherit": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -2567,6 +2600,15 @@
|
||||
"description": "Start the TUI in raw scrollback mode for copy-friendly transcript output. Defaults to `false`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"session_picker_view": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionPickerViewMode"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Preferred layout for resume/fork session picker results."
|
||||
},
|
||||
"show_tooltips": {
|
||||
"default": true,
|
||||
"description": "Show startup tooltips in the TUI welcome screen. Defaults to `true`.",
|
||||
|
||||
@@ -44,6 +44,7 @@ use codex_config::types::NotificationCondition;
|
||||
use codex_config::types::NotificationMethod;
|
||||
use codex_config::types::Notifications;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::SkillsConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverableType;
|
||||
@@ -556,6 +557,7 @@ fn config_toml_deserializes_model_availability_nux() {
|
||||
status_line_use_colors: true,
|
||||
terminal_title: None,
|
||||
theme: None,
|
||||
session_picker_view: None,
|
||||
keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig {
|
||||
shown_count: HashMap::from([
|
||||
@@ -2156,6 +2158,31 @@ fn tui_theme_defaults_to_none() {
|
||||
assert_eq!(parsed.tui.as_ref().and_then(|t| t.theme.as_deref()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_session_picker_view_deserializes_from_toml() {
|
||||
let cfg = r#"
|
||||
[tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
let parsed = toml::from_str::<ConfigToml>(cfg).expect("TOML deserialization should succeed");
|
||||
assert_eq!(
|
||||
parsed.tui.as_ref().and_then(|t| t.session_picker_view),
|
||||
Some(SessionPickerViewMode::Dense),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_session_picker_view_defaults_to_none() {
|
||||
let cfg = r#"
|
||||
[tui]
|
||||
"#;
|
||||
let parsed = toml::from_str::<ConfigToml>(cfg).expect("TOML deserialization should succeed");
|
||||
assert_eq!(
|
||||
parsed.tui.as_ref().and_then(|t| t.session_picker_view),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
let cfg = r#"
|
||||
@@ -2179,6 +2206,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
status_line_use_colors: true,
|
||||
terminal_title: None,
|
||||
theme: None,
|
||||
session_picker_view: None,
|
||||
keymap: TuiKeymap::default(),
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
terminal_resize_reflow_max_rows: None,
|
||||
@@ -2244,6 +2272,78 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_tui_rejects_unsupported_settings() {
|
||||
let err = toml::from_str::<ConfigToml>(
|
||||
r#"profile = "work"
|
||||
|
||||
[profiles.work.tui]
|
||||
theme = "dark"
|
||||
"#,
|
||||
)
|
||||
.expect_err("profile TUI config should only accept supported fields");
|
||||
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
assert!(err.to_string().contains("theme"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_config_resolves_session_picker_view_default_and_override() {
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml::default(),
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load default config");
|
||||
|
||||
assert_eq!(cfg.tui_session_picker_view, SessionPickerViewMode::Dense);
|
||||
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
tui: Some(Tui {
|
||||
session_picker_view: Some(SessionPickerViewMode::Comfortable),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load root override config");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tui_session_picker_view,
|
||||
SessionPickerViewMode::Comfortable
|
||||
);
|
||||
|
||||
let cfg_toml = toml::from_str::<ConfigToml>(
|
||||
r#"profile = "work"
|
||||
|
||||
[tui]
|
||||
session_picker_view = "dense"
|
||||
|
||||
[profiles.work.tui]
|
||||
session_picker_view = "comfortable"
|
||||
"#,
|
||||
)
|
||||
.expect("parse profile scoped tui config");
|
||||
|
||||
let cfg = Config::load_from_base_config_with_overrides(
|
||||
cfg_toml,
|
||||
ConfigOverrides::default(),
|
||||
tempdir().expect("tempdir").abs(),
|
||||
)
|
||||
.await
|
||||
.expect("load profile override config");
|
||||
|
||||
assert_eq!(
|
||||
cfg.tui_session_picker_view,
|
||||
SessionPickerViewMode::Comfortable
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sandbox_config_parsing() {
|
||||
let sandbox_full_access = r#"
|
||||
@@ -6511,6 +6611,7 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
},
|
||||
o3_profile_config
|
||||
@@ -6714,6 +6815,7 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -6871,6 +6973,7 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
@@ -7013,6 +7116,7 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
tui_status_line_use_colors: true,
|
||||
tui_terminal_title: None,
|
||||
tui_theme: None,
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
otel: OtelConfig::default(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::path_utils::write_atomically;
|
||||
use anyhow::Context;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_features::FEATURES;
|
||||
use codex_protocol::config_types::Personality;
|
||||
@@ -91,6 +92,14 @@ pub fn syntax_theme_edit(name: &str) -> ConfigEdit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a config edit that sets `[tui].session_picker_view = "<mode>"`.
|
||||
pub fn session_picker_view_edit(mode: SessionPickerViewMode) -> ConfigEdit {
|
||||
ConfigEdit::SetPath {
|
||||
segments: vec!["tui".to_string(), "session_picker_view".to_string()],
|
||||
value: value(mode.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces a config edit that sets `[tui].status_line` to an explicit ordered list.
|
||||
///
|
||||
/// The array is written even when it is empty so "hide the status line" stays
|
||||
@@ -1316,6 +1325,25 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_session_picker_view(mut self, mode: SessionPickerViewMode) -> Self {
|
||||
let segments = if let Some(profile) = self.profile.as_ref() {
|
||||
vec![
|
||||
"profiles".to_string(),
|
||||
profile.clone(),
|
||||
"tui".to_string(),
|
||||
"session_picker_view".to_string(),
|
||||
]
|
||||
} else {
|
||||
vec!["tui".to_string(), "session_picker_view".to_string()]
|
||||
};
|
||||
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments,
|
||||
value: value(mode.to_string()),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_edits<I>(mut self, edits: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = ConfigEdit>,
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use codex_config::types::AppToolApproval;
|
||||
use codex_config::types::McpServerToolConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(unix)]
|
||||
@@ -48,6 +49,41 @@ fn builder_with_edits_applies_custom_paths() {
|
||||
assert_eq!(contents, "enabled = true\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_picker_view_edit_writes_root_tui_setting() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([session_picker_view_edit(SessionPickerViewMode::Dense)])
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_picker_view_builder_respects_active_profile() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_profile(Some("work"))
|
||||
.set_session_picker_view(SessionPickerViewMode::Dense)
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[profiles.work.tui]
|
||||
session_picker_view = "dense"
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_binding_edit_writes_root_action_binding() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
@@ -49,6 +49,7 @@ use codex_config::types::OAuthCredentialsStoreMode;
|
||||
use codex_config::types::OtelConfig;
|
||||
use codex_config::types::OtelConfigToml;
|
||||
use codex_config::types::OtelExporterKind;
|
||||
use codex_config::types::SessionPickerViewMode;
|
||||
use codex_config::types::ToolSuggestConfig;
|
||||
use codex_config::types::ToolSuggestDisabledTool;
|
||||
use codex_config::types::ToolSuggestDiscoverable;
|
||||
@@ -547,6 +548,9 @@ pub struct Config {
|
||||
/// Syntax highlighting theme override (kebab-case name).
|
||||
pub tui_theme: Option<String>,
|
||||
|
||||
/// Preferred layout for resume/fork session picker results.
|
||||
pub tui_session_picker_view: SessionPickerViewMode,
|
||||
|
||||
/// Terminal resize-reflow tuning knobs.
|
||||
pub terminal_resize_reflow: TerminalResizeReflowConfig,
|
||||
|
||||
@@ -3168,6 +3172,12 @@ impl Config {
|
||||
.unwrap_or(true),
|
||||
tui_terminal_title: cfg.tui.as_ref().and_then(|t| t.terminal_title.clone()),
|
||||
tui_theme: cfg.tui.as_ref().and_then(|t| t.theme.clone()),
|
||||
tui_session_picker_view: config_profile
|
||||
.tui
|
||||
.as_ref()
|
||||
.and_then(|t| t.session_picker_view)
|
||||
.or_else(|| cfg.tui.as_ref().and_then(|t| t.session_picker_view))
|
||||
.unwrap_or_default(),
|
||||
terminal_resize_reflow,
|
||||
tui_keymap: cfg
|
||||
.tui
|
||||
|
||||
@@ -40,6 +40,7 @@ use codex_core_api::Permissions;
|
||||
use codex_core_api::ProjectConfig;
|
||||
use codex_core_api::RealtimeAudioConfig;
|
||||
use codex_core_api::RealtimeConfig;
|
||||
use codex_core_api::SessionPickerViewMode;
|
||||
use codex_core_api::SessionSource;
|
||||
use codex_core_api::ShellEnvironmentPolicy;
|
||||
use codex_core_api::TerminalResizeReflowConfig;
|
||||
@@ -200,6 +201,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
tui_raw_output_mode: false,
|
||||
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
||||
tui_keymap: TuiKeymap::default(),
|
||||
tui_session_picker_view: SessionPickerViewMode::Dense,
|
||||
tui_vim_mode_default: false,
|
||||
cwd,
|
||||
cli_auth_credentials_store_mode: AuthCredentialsStoreMode::File,
|
||||
|
||||
@@ -77,7 +77,7 @@ impl App {
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
};
|
||||
match crate::resume_picker::run_resume_picker_with_app_server(
|
||||
match crate::resume_picker::run_resume_picker_from_existing_session_with_app_server(
|
||||
tui,
|
||||
&self.config,
|
||||
/*show_all*/ false,
|
||||
@@ -97,9 +97,13 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
SessionSelection::Exit
|
||||
| SessionSelection::StartFresh
|
||||
| SessionSelection::Fork(_) => {}
|
||||
SessionSelection::Exit | SessionSelection::StartFresh => {
|
||||
self.refresh_in_memory_config_from_disk_best_effort(
|
||||
"closing the session picker",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
SessionSelection::Fork(_) => {}
|
||||
}
|
||||
|
||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||
|
||||
@@ -1430,6 +1430,11 @@ async fn run_ratatui_app(
|
||||
None => None,
|
||||
};
|
||||
|
||||
let picker_cancelled_without_selection = matches!(
|
||||
session_selection,
|
||||
resume_picker::SessionSelection::StartFresh
|
||||
) && (cli.resume_picker || cli.fork_picker);
|
||||
|
||||
let mut config = match &session_selection {
|
||||
resume_picker::SessionSelection::Resume(_) | resume_picker::SessionSelection::Fork(_) => {
|
||||
load_config_or_exit_with_fallback_cwd(
|
||||
@@ -1440,6 +1445,14 @@ async fn run_ratatui_app(
|
||||
)
|
||||
.await
|
||||
}
|
||||
resume_picker::SessionSelection::StartFresh if picker_cancelled_without_selection => {
|
||||
load_config_or_exit(
|
||||
cli_kv_overrides.clone(),
|
||||
overrides.clone(),
|
||||
cloud_requirements.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => config,
|
||||
};
|
||||
|
||||
|
||||
+4524
-567
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,214 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::history_cell::AgentMarkdownCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::PlainHistoryCell;
|
||||
use crate::history_cell::ReasoningSummaryCell;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use ratatui::style::Stylize as _;
|
||||
use ratatui::text::Line;
|
||||
|
||||
pub(crate) type TranscriptCells = Vec<Arc<dyn HistoryCell>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum RawReasoningVisibility {
|
||||
Hidden,
|
||||
Visible,
|
||||
}
|
||||
|
||||
pub(crate) async fn load_session_transcript(
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
raw_reasoning_visibility: RawReasoningVisibility,
|
||||
) -> std::io::Result<TranscriptCells> {
|
||||
let thread = app_server
|
||||
.thread_read(thread_id, /*include_turns*/ true)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
Ok(thread_to_transcript_cells(
|
||||
&thread,
|
||||
raw_reasoning_visibility,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn thread_to_transcript_cells(
|
||||
thread: &Thread,
|
||||
raw_reasoning_visibility: RawReasoningVisibility,
|
||||
) -> TranscriptCells {
|
||||
let cwd = thread.cwd.as_path();
|
||||
let mut cells: TranscriptCells = Vec::new();
|
||||
for item in thread.turns.iter().flat_map(|turn| turn.items.iter()) {
|
||||
match item {
|
||||
ThreadItem::UserMessage { id, content } => {
|
||||
let item = UserMessageItem {
|
||||
id: id.clone(),
|
||||
content: content
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(codex_app_server_protocol::UserInput::into_core)
|
||||
.collect(),
|
||||
};
|
||||
cells.push(Arc::new(UserHistoryCell {
|
||||
message: item.message(),
|
||||
text_elements: item.text_elements(),
|
||||
local_image_paths: item.local_image_paths(),
|
||||
remote_image_urls: item.image_urls(),
|
||||
}));
|
||||
}
|
||||
ThreadItem::AgentMessage { text, .. } => {
|
||||
if !text.trim().is_empty() {
|
||||
cells.push(Arc::new(AgentMarkdownCell::new(text.clone(), cwd)));
|
||||
}
|
||||
}
|
||||
ThreadItem::Plan { text, .. } => {
|
||||
if !text.trim().is_empty() {
|
||||
cells.push(Arc::new(crate::history_cell::new_proposed_plan(
|
||||
text.clone(),
|
||||
cwd,
|
||||
)));
|
||||
}
|
||||
}
|
||||
ThreadItem::Reasoning {
|
||||
summary, content, ..
|
||||
} => {
|
||||
let text = if matches!(raw_reasoning_visibility, RawReasoningVisibility::Visible)
|
||||
&& !content.is_empty()
|
||||
{
|
||||
content.join("\n\n")
|
||||
} else {
|
||||
summary.join("\n\n")
|
||||
};
|
||||
if !text.trim().is_empty() {
|
||||
cells.push(Arc::new(ReasoningSummaryCell::new(
|
||||
"Reasoning".to_string(),
|
||||
text,
|
||||
cwd,
|
||||
/*transcript_only*/ false,
|
||||
)));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
if let Some(cell) = fallback_transcript_cell(other) {
|
||||
cells.push(Arc::new(cell));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cells.is_empty() {
|
||||
cells.push(Arc::new(PlainHistoryCell::new(vec![
|
||||
"No transcript content available".italic().dim().into(),
|
||||
])));
|
||||
}
|
||||
cells
|
||||
}
|
||||
|
||||
fn fallback_transcript_cell(item: &ThreadItem) -> Option<PlainHistoryCell> {
|
||||
let lines = match item {
|
||||
ThreadItem::HookPrompt { fragments, .. } => fragments
|
||||
.iter()
|
||||
.map(|fragment| {
|
||||
vec![
|
||||
"hook prompt: ".dim(),
|
||||
fragment.text.trim().to_string().into(),
|
||||
]
|
||||
.into()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
ThreadItem::CommandExecution {
|
||||
command,
|
||||
status,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
..
|
||||
} => {
|
||||
let mut lines: Vec<Line<'static>> =
|
||||
vec![vec!["$ ".dim(), command.clone().into()].into()];
|
||||
lines.push(
|
||||
format!(
|
||||
"status: {status:?}{}",
|
||||
exit_code
|
||||
.map(|code| format!(" · exit {code}"))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.dim()
|
||||
.into(),
|
||||
);
|
||||
if let Some(output) = aggregated_output.as_deref()
|
||||
&& !output.trim().is_empty()
|
||||
{
|
||||
lines.extend(
|
||||
output
|
||||
.lines()
|
||||
.map(|line| vec![" ".dim(), line.trim_end().to_string().dim()].into()),
|
||||
);
|
||||
}
|
||||
lines
|
||||
}
|
||||
ThreadItem::FileChange {
|
||||
changes, status, ..
|
||||
} => vec![
|
||||
format!("file changes: {status:?} · {} changes", changes.len())
|
||||
.dim()
|
||||
.into(),
|
||||
],
|
||||
ThreadItem::McpToolCall {
|
||||
server,
|
||||
tool,
|
||||
status,
|
||||
..
|
||||
} => vec![
|
||||
format!("mcp tool: {server}/{tool} · {status:?}")
|
||||
.dim()
|
||||
.into(),
|
||||
],
|
||||
ThreadItem::DynamicToolCall {
|
||||
namespace,
|
||||
tool,
|
||||
status,
|
||||
..
|
||||
} => {
|
||||
let name = namespace
|
||||
.as_ref()
|
||||
.map(|namespace| format!("{namespace}/{tool}"))
|
||||
.unwrap_or_else(|| tool.clone());
|
||||
vec![format!("tool: {name} · {status:?}").dim().into()]
|
||||
}
|
||||
ThreadItem::CollabAgentToolCall { tool, status, .. } => {
|
||||
vec![format!("agent tool: {tool:?} · {status:?}").dim().into()]
|
||||
}
|
||||
ThreadItem::WebSearch { query, .. } => {
|
||||
vec![vec!["web search: ".dim(), query.clone().into()].into()]
|
||||
}
|
||||
ThreadItem::ImageView { path, .. } => {
|
||||
vec![format!("image: {}", path.as_path().display()).dim().into()]
|
||||
}
|
||||
ThreadItem::ImageGeneration {
|
||||
status, saved_path, ..
|
||||
} => {
|
||||
let saved = saved_path
|
||||
.as_ref()
|
||||
.map(|path| format!(" · {}", path.as_path().display()))
|
||||
.unwrap_or_default();
|
||||
vec![format!("image generation: {status}{saved}").dim().into()]
|
||||
}
|
||||
ThreadItem::EnteredReviewMode { review, .. } => {
|
||||
vec![vec!["review started: ".dim(), review.clone().into()].into()]
|
||||
}
|
||||
ThreadItem::ExitedReviewMode { review, .. } => {
|
||||
vec![vec!["review finished: ".dim(), review.clone().into()].into()]
|
||||
}
|
||||
ThreadItem::ContextCompaction { .. } => {
|
||||
vec!["context compacted".dim().into()]
|
||||
}
|
||||
ThreadItem::UserMessage { .. }
|
||||
| ThreadItem::AgentMessage { .. }
|
||||
| ThreadItem::Plan { .. }
|
||||
| ThreadItem::Reasoning { .. } => return None,
|
||||
};
|
||||
(!lines.is_empty()).then(|| PlainHistoryCell::new(lines))
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 120,)"
|
||||
---
|
||||
❯ 15m ago Propose session picker redesign with enough title text to exercise truncation
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 100,)"
|
||||
---
|
||||
❯ 15m ago Propose session picker redesign with enough title text to exercise truncation
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 48,)"
|
||||
---
|
||||
❯ 15m ago Propose session picker redesig...
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(false,\nSome(PathBuf::from(\"/Users/felipe.coury/code/codex.fcoury-session-picker/codex-rs\")),\n100,)"
|
||||
---
|
||||
❯ 15m ago Propose session picker redesign with enough title text to exercise truncation
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 48,)"
|
||||
---
|
||||
❯ 15m ago Propose session picker redesig...
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: terminal.backend().to_string()
|
||||
---
|
||||
15m ago First dense row
|
||||
❯ 15m ago Second dense row
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: rendered
|
||||
---
|
||||
⌄ Investigate picker expansion
|
||||
│ Session: 019dabc1-0ef5-7431-b81c-03037f51f62c
|
||||
│ Created: 1 hour ago · 2026-04-28 16:30:00
|
||||
│ Updated: 15 minutes ago · 2026-04-28 17:45:00
|
||||
│ Directory: /tmp/codex
|
||||
│ Branch: fcoury/session-picker
|
||||
│
|
||||
│ Conversation:
|
||||
│ Show me the recent transcript
|
||||
└ Here are the last few lines.
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "footer_snapshot(&state, 96, 20)"
|
||||
---
|
||||
───────────────────────────────────────────────────────────────────────────────── 0 / 0 · 100% ─
|
||||
enter resume esc clear ctrl+c quit tab focus ←/→ option
|
||||
ctrl+o comfy ctrl+t preview ctrl+e exp ↑/↓ browse
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "footer_snapshot(&state, 220, 20)"
|
||||
---
|
||||
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0 / 0 · 100% ─
|
||||
enter resume esc start new ctrl+c quit tab focus sort/filter ←/→ change option
|
||||
ctrl+o dense view ctrl+t transcript ctrl+e expand ↑/↓ browse
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: terminal.backend().to_string()
|
||||
---
|
||||
↑ more
|
||||
❯ item-2
|
||||
10m ago ⌁ no cwd no branch
|
||||
|
||||
item-3
|
||||
↓ more
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: terminal.backend().to_string()
|
||||
---
|
||||
❯ Investigate picker expansion
|
||||
15m ago ⌁ /tmp/codex
|
||||
fcoury/session-picker
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: terminal.backend().to_string()
|
||||
---
|
||||
Type to search Filter: [Cwd] All Sort: [Updated] Created
|
||||
+8
-4
@@ -2,7 +2,11 @@
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: snapshot
|
||||
---
|
||||
Created Updated Branch CWD Conversation
|
||||
16 minutes ago 42 seconds ago - - Fix resume picker timestamps
|
||||
> 1 hour ago 35 minutes ago - - Investigate lazy pagination cap
|
||||
2 hours ago 2 hours ago - - Explain the codebase
|
||||
Fix resume picker timestamps
|
||||
42s ago ⌁ no cwd no branch
|
||||
|
||||
❯ Investigate lazy pagination cap
|
||||
35m ago ⌁ no cwd no branch
|
||||
|
||||
Explain the codebase
|
||||
2h ago ⌁ no cwd no branch
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: snapshot
|
||||
---
|
||||
❯ Find pending threads and emails
|
||||
- ⌁ no cwd no branch
|
||||
|
||||
Plan raw scrollback mod Loading transcript…
|
||||
- ⌁ no cwd branch
|
||||
Reference in New Issue
Block a user