Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: tui/src/app/thread_goal_actions.rs
expression: terminal.backend()
---


■ Goals need a saved session. This session is temporary.
Run `codex` to start a saved session, or `codex resume` / `/resume` to
reopen one.
91 changes: 84 additions & 7 deletions codex-rs/tui/src/app/thread_goal_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ use crate::goal_display::goal_usage_summary;
use codex_app_server_protocol::ThreadGoalStatus;
use codex_protocol::ThreadId;

const EPHEMERAL_THREAD_GOAL_ERROR_MESSAGE: &str = concat!(
"Goals need a saved session. This session is temporary.\n",
"Run `codex` to start a saved session, or `codex resume` / `/resume` to reopen one.",
Comment thread
etraut-openai marked this conversation as resolved.
);

impl App {
pub(super) async fn open_thread_goal_menu(
&mut self,
Expand All @@ -26,7 +31,7 @@ impl App {
Ok(response) => response,
Err(err) => {
self.chat_widget
.add_error_message(format!("Failed to read thread goal: {err}"));
.add_error_message(thread_goal_error_message("read", &err));
return;
}
};
Expand Down Expand Up @@ -91,7 +96,7 @@ impl App {
Ok(response) => response,
Err(err) => {
self.chat_widget
.add_error_message(format!("Failed to read thread goal: {err}"));
.add_error_message(thread_goal_error_message("read", &err));
return;
}
};
Expand Down Expand Up @@ -125,7 +130,7 @@ impl App {
Ok(_) => {}
Err(err) => {
self.chat_widget
.add_error_message(format!("Failed to read thread goal: {err}"));
.add_error_message(thread_goal_error_message("read", &err));
return;
}
}
Expand All @@ -140,7 +145,7 @@ impl App {
return;
}
self.chat_widget
.add_error_message(format!("Failed to replace thread goal: {err}"));
.add_error_message(thread_goal_error_message("replace", &err));
return;
}
}
Expand Down Expand Up @@ -170,7 +175,7 @@ impl App {
Err(err) => {
let action = if replacing_goal { "replace" } else { "set" };
self.chat_widget
.add_error_message(format!("Failed to {action} thread goal: {err}"));
.add_error_message(thread_goal_error_message(action, &err));
}
}
}
Expand Down Expand Up @@ -200,7 +205,7 @@ impl App {
),
Err(err) => self
.chat_widget
.add_error_message(format!("Failed to update thread goal: {err}")),
.add_error_message(thread_goal_error_message("update", &err)),
}
}

Expand Down Expand Up @@ -228,7 +233,7 @@ impl App {
}
Err(err) => self
.chat_widget
.add_error_message(format!("Failed to clear thread goal: {err}")),
.add_error_message(thread_goal_error_message("clear", &err)),
}
}

Expand Down Expand Up @@ -274,3 +279,75 @@ impl App {
);
}
}

fn thread_goal_error_message(action: &str, err: &color_eyre::Report) -> String {
if is_ephemeral_thread_goal_error(err) {
EPHEMERAL_THREAD_GOAL_ERROR_MESSAGE.to_string()
} else {
format!("Failed to {action} thread goal: {err}")
}
}

fn is_ephemeral_thread_goal_error(err: &color_eyre::Report) -> bool {
err.chain().any(|cause| {
let message = cause.to_string();
message.contains("ephemeral thread does not support goals")
|| message.contains("thread goals require a persisted thread; this thread is ephemeral")
})
}

#[cfg(test)]
mod tests {
use crate::history_cell::HistoryCell;
use pretty_assertions::assert_eq;
use ratatui::layout::Rect;

use super::*;

#[test]
fn thread_goal_error_message_explains_temporary_session() {
let err = color_eyre::eyre::eyre!(
"thread/goal/get failed: ephemeral thread does not support goals: thread-1"
)
.wrap_err("thread/goal/get failed in TUI");

assert_eq!(
thread_goal_error_message("read", &err),
EPHEMERAL_THREAD_GOAL_ERROR_MESSAGE
);
}

#[test]
fn thread_goal_ephemeral_error_message_renders_snapshot() {
let err = color_eyre::eyre::eyre!(
"thread/goal/get failed: ephemeral thread does not support goals: thread-1"
)
.wrap_err("thread/goal/get failed in TUI");
let cell = crate::history_cell::new_error_event(thread_goal_error_message("read", &err));
let width = 72;
let height = 6;
let backend = crate::test_backend::VT100Backend::new(width, height);
let mut terminal =
crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
terminal.set_viewport_area(Rect::new(0, height - 1, width, 1));

crate::insert_history::insert_history_lines(
&mut terminal,
cell.display_lines(/*width*/ width),
)
.expect("insert history lines");

insta::assert_snapshot!(terminal.backend());
}

#[test]
fn thread_goal_error_message_preserves_generic_failure_context() {
let err =
color_eyre::eyre::eyre!("server disappeared").wrap_err("thread/goal/get failed in TUI");

assert_eq!(
thread_goal_error_message("read", &err),
"Failed to read thread goal: thread/goal/get failed in TUI"
);
}
}
Loading