mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Add a TUI setting to disable automatic recaps (#42101)
## What changed - Add the `tui.auto_recap` configuration option, enabled by default. - When disabled, cancel scheduled automatic recap checks, reject automatic requests, and discard pending automatic results without retrying. - Keep manual `/recap` requests available regardless of the setting. ## Testing - Cover configuration defaults and command-line overrides. - Cover scheduling cancellation, pending request cleanup, discarded results, and manual recaps while automatic recaps are disabled. GitOrigin-RevId: 7d5323f58a2c84e60683a25a8c77653097efe6e2
This commit is contained in:
@@ -736,6 +736,11 @@ pub struct Tui {
|
||||
#[serde(default = "default_true")]
|
||||
pub show_tooltips: bool,
|
||||
|
||||
/// Generate automatic conversation recaps when the terminal is unfocused.
|
||||
/// Defaults to `true`. Disabling this leaves `/recap` available on demand.
|
||||
#[serde(default = "default_true")]
|
||||
pub auto_recap: bool,
|
||||
|
||||
/// When true, disables burst-paste detection for typed input entirely.
|
||||
/// All characters are inserted as they are received, and no buffering
|
||||
/// or placeholder replacement will occur for fast keypress bursts.
|
||||
|
||||
@@ -3810,6 +3810,11 @@
|
||||
"description": "Enable animations (welcome screen, shimmer effects, spinners). Defaults to `true`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"auto_recap": {
|
||||
"default": true,
|
||||
"description": "Generate automatic conversation recaps when the terminal is unfocused. Defaults to `true`. Disabling this leaves `/recap` available on demand.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disable_paste_burst": {
|
||||
"description": "When true, disables burst-paste detection for typed input entirely. All characters are inserted as they are received, and no buffering or placeholder replacement will occur for fast keypress bursts. Overrides the legacy top-level `disable_paste_burst` setting. Defaults to `false`.",
|
||||
"type": "boolean"
|
||||
|
||||
@@ -1210,6 +1210,7 @@ fn config_toml_deserializes_model_availability_nux() {
|
||||
notification_settings: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
auto_recap: true,
|
||||
disable_paste_burst: None,
|
||||
vim_mode_default: false,
|
||||
raw_output_mode: false,
|
||||
@@ -1316,6 +1317,38 @@ async fn runtime_config_uses_tui_raw_output_mode() {
|
||||
assert!(cfg.tui_raw_output_mode);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn tui_auto_recap_defaults_and_cli_overrides() -> anyhow::Result<()> {
|
||||
for (toml, override_value, expected) in [
|
||||
("", None, true),
|
||||
("[tui]", None, true),
|
||||
("[tui]\nauto_recap = true", None, true),
|
||||
("[tui]\nauto_recap = false", None, false),
|
||||
("[tui]\nauto_recap = true", Some(false), false),
|
||||
("[tui]\nauto_recap = false", Some(true), true),
|
||||
] {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(codex_home.path().join(CONFIG_TOML_FILE), toml)?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cli_overrides(
|
||||
override_value
|
||||
.into_iter()
|
||||
.map(|enabled| ("tui.auto_recap".to_string(), TomlValue::Boolean(enabled)))
|
||||
.collect(),
|
||||
)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.tui_auto_recap, expected,
|
||||
"config: {toml}, override: {override_value:?}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_deserializes_permission_profiles() {
|
||||
let toml = r#"
|
||||
@@ -4190,6 +4223,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
|
||||
notification_settings: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
auto_recap: true,
|
||||
disable_paste_burst: None,
|
||||
vim_mode_default: false,
|
||||
raw_output_mode: false,
|
||||
|
||||
@@ -742,6 +742,9 @@ pub struct Config {
|
||||
/// Show startup tooltips in the TUI welcome screen.
|
||||
pub show_tooltips: bool,
|
||||
|
||||
/// Generate automatic TUI recaps. Manual `/recap` remains available when disabled.
|
||||
pub tui_auto_recap: bool,
|
||||
|
||||
/// Persisted startup availability NUX state for model tooltips.
|
||||
pub model_availability_nux: ModelAvailabilityNuxConfig,
|
||||
|
||||
@@ -4322,6 +4325,7 @@ impl Config {
|
||||
.unwrap_or_default(),
|
||||
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
|
||||
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
|
||||
tui_auto_recap: cfg.tui.as_ref().map(|t| t.auto_recap).unwrap_or(/*default*/ true),
|
||||
model_availability_nux: cfg
|
||||
.tui
|
||||
.as_ref()
|
||||
|
||||
@@ -224,6 +224,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
|
||||
tui_notifications: TuiNotificationSettings::default(),
|
||||
animations: true,
|
||||
show_tooltips: true,
|
||||
tui_auto_recap: true,
|
||||
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
||||
tui_alternate_screen: AltScreenMode::Auto,
|
||||
tui_status_line: None,
|
||||
|
||||
@@ -833,8 +833,7 @@ impl App {
|
||||
self.recap.note_focus_lost(now);
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
self.recap
|
||||
.schedule_check(thread_id, self.app_event_tx.clone(), now);
|
||||
self.schedule_recap_check(thread_id, now);
|
||||
}
|
||||
}
|
||||
TuiEvent::FocusGained => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Determines when an unfocused conversation is ready for an automatic recap.
|
||||
//! The TUI opt-out suppresses automatic scheduling and requests, but not manual `/recap`.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -176,6 +177,33 @@ pub(super) struct RecapRequest {
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn recap_trigger_enabled(&self, trigger: RecapTrigger) -> bool {
|
||||
match trigger {
|
||||
RecapTrigger::Automatic => self.config.tui_auto_recap,
|
||||
RecapTrigger::Manual => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn schedule_recap_check(&mut self, thread_id: ThreadId, now: Instant) {
|
||||
if let Some(task) = self.recap.scheduled_check.take() {
|
||||
task.abort();
|
||||
}
|
||||
|
||||
if !self.config.tui_auto_recap {
|
||||
return;
|
||||
}
|
||||
let Some(deadline) = self.recap.next_check_deadline() else {
|
||||
return;
|
||||
};
|
||||
let delay = deadline.saturating_duration_since(now);
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
|
||||
self.recap.scheduled_check = Some(tokio::spawn(async move {
|
||||
tokio::time::sleep(delay).await;
|
||||
app_event_tx.send(AppEvent::CheckRecap { thread_id });
|
||||
}));
|
||||
}
|
||||
|
||||
pub(super) fn clear_recap_request(&mut self, trigger: RecapTrigger) {
|
||||
self.recap.clear_in_flight_request();
|
||||
|
||||
@@ -200,6 +228,9 @@ impl App {
|
||||
}
|
||||
|
||||
fn retry_or_report_recap_failure(&mut self, request: RecapRequest) {
|
||||
if !self.recap_trigger_enabled(request.trigger) {
|
||||
return;
|
||||
}
|
||||
match request.trigger {
|
||||
RecapTrigger::Automatic => self.recap.schedule_retry(
|
||||
request.thread_id,
|
||||
@@ -218,6 +249,9 @@ impl App {
|
||||
thread_id: ThreadId,
|
||||
trigger: RecapTrigger,
|
||||
) {
|
||||
if !self.recap_trigger_enabled(trigger) {
|
||||
return;
|
||||
}
|
||||
if self.recap.in_flight_request.is_some() {
|
||||
if matches!(trigger, RecapTrigger::Manual) {
|
||||
self.chat_widget
|
||||
@@ -332,6 +366,7 @@ impl App {
|
||||
&& !self.chat_widget.is_user_turn_pending_or_running()
|
||||
&& self.recap.completed_turns == completed_turn_count
|
||||
&& self.recap.turn_revision == turn_revision
|
||||
&& self.recap_trigger_enabled(trigger)
|
||||
&& trigger_is_eligible;
|
||||
let Ok(temporary_thread_id) = ThreadId::from_string(&temporary_thread_id_text) else {
|
||||
if is_current_request {
|
||||
@@ -408,7 +443,8 @@ impl App {
|
||||
|
||||
self.clear_recap_request(trigger);
|
||||
|
||||
if self.current_displayed_thread_id() != Some(thread_id)
|
||||
if !self.recap_trigger_enabled(trigger)
|
||||
|| self.current_displayed_thread_id() != Some(thread_id)
|
||||
|| self.chat_widget.is_user_turn_pending_or_running()
|
||||
|| self.recap.completed_turns != completed_turn_count
|
||||
|| self.recap.turn_revision != turn_revision
|
||||
@@ -541,28 +577,6 @@ impl RecapState {
|
||||
self.last_turn_finished_at = Some(now);
|
||||
}
|
||||
|
||||
pub(super) fn schedule_check(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
app_event_tx: AppEventSender,
|
||||
now: Instant,
|
||||
) {
|
||||
if let Some(task) = self.scheduled_check.take() {
|
||||
task.abort();
|
||||
}
|
||||
|
||||
let Some(deadline) = self.next_check_deadline() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let delay = deadline.saturating_duration_since(now);
|
||||
|
||||
self.scheduled_check = Some(tokio::spawn(async move {
|
||||
tokio::time::sleep(delay).await;
|
||||
app_event_tx.send(AppEvent::CheckRecap { thread_id });
|
||||
}));
|
||||
}
|
||||
|
||||
pub(super) fn schedule_retry(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
|
||||
@@ -318,14 +318,15 @@ fn regaining_focus_preserves_only_manual_in_flight_request() {
|
||||
async fn scheduled_check_fires_at_recap_deadline() {
|
||||
let thread_id = ThreadId::new();
|
||||
let now = Instant::now();
|
||||
let mut state = RecapState::default();
|
||||
let mut app = make_test_app().await;
|
||||
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
app.app_event_tx = AppEventSender::new(event_tx);
|
||||
|
||||
state.note_focus_lost(now);
|
||||
app.recap.note_focus_lost(now);
|
||||
for _ in 0..3 {
|
||||
state.note_turn_finished(&TurnStatus::Completed, now);
|
||||
app.recap.note_turn_finished(&TurnStatus::Completed, now);
|
||||
}
|
||||
state.schedule_check(thread_id, AppEventSender::new(event_tx), now);
|
||||
app.schedule_recap_check(thread_id, now);
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
tokio::time::advance(RECAP_DELAY - Duration::from_secs(/*secs*/ 1)).await;
|
||||
@@ -346,22 +347,24 @@ async fn scheduled_check_fires_at_recap_deadline() {
|
||||
async fn rescheduling_check_cancels_the_earlier_timer() {
|
||||
let thread_id = ThreadId::new();
|
||||
let first_turn = Instant::now();
|
||||
let mut state = RecapState::default();
|
||||
let mut app = make_test_app().await;
|
||||
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let app_event_tx = AppEventSender::new(event_tx);
|
||||
app.app_event_tx = AppEventSender::new(event_tx);
|
||||
|
||||
state.note_focus_lost(first_turn);
|
||||
app.recap.note_focus_lost(first_turn);
|
||||
for _ in 0..3 {
|
||||
state.note_turn_finished(&TurnStatus::Completed, first_turn);
|
||||
app.recap
|
||||
.note_turn_finished(&TurnStatus::Completed, first_turn);
|
||||
}
|
||||
state.schedule_check(thread_id, app_event_tx.clone(), first_turn);
|
||||
app.schedule_recap_check(thread_id, first_turn);
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
let elapsed = Duration::from_secs(/*secs*/ 1);
|
||||
tokio::time::advance(elapsed).await;
|
||||
let later_turn = first_turn + elapsed;
|
||||
state.note_turn_finished(&TurnStatus::Completed, later_turn);
|
||||
state.schedule_check(thread_id, app_event_tx, later_turn);
|
||||
app.recap
|
||||
.note_turn_finished(&TurnStatus::Completed, later_turn);
|
||||
app.schedule_recap_check(thread_id, later_turn);
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
tokio::time::advance(RECAP_DELAY - elapsed).await;
|
||||
@@ -378,6 +381,32 @@ async fn rescheduling_check_cancels_the_earlier_timer() {
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||
async fn auto_recap_opt_out_cancels_scheduling_after_restored_progress() {
|
||||
let thread_id = ThreadId::new();
|
||||
let now = Instant::now();
|
||||
let mut app = make_test_app().await;
|
||||
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
app.app_event_tx = AppEventSender::new(event_tx);
|
||||
app.recap.note_focus_lost(now);
|
||||
let progress = RecapProgress {
|
||||
completed_turns: 3,
|
||||
last_recapped_turn_count: None,
|
||||
};
|
||||
app.recap.seed_from_progress(progress, now);
|
||||
app.schedule_recap_check(thread_id, now);
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
app.config.tui_auto_recap = false;
|
||||
app.schedule_recap_check(thread_id, now);
|
||||
tokio::time::advance(RECAP_DELAY).await;
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
assert!(app.recap.scheduled_check.is_none());
|
||||
assert!(matches!(event_rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
assert_eq!(app.recap.progress(), progress);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread", start_paused = true)]
|
||||
async fn dropping_recap_state_cancels_timer() {
|
||||
let mut state = RecapState::default();
|
||||
@@ -620,6 +649,27 @@ async fn generated_recap_is_returned_for_synchronous_insertion() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_recap_opt_out_discards_results_without_retrying() {
|
||||
let thread_id = ThreadId::new();
|
||||
let mut app = app_with_visible_thread(thread_id).await;
|
||||
for result in [
|
||||
Ok(serde_json::json!({ "recap": "obsolete" }).to_string()),
|
||||
Err("temporary failure".to_string()),
|
||||
] {
|
||||
let (request, temporary_thread_id) = track_in_flight_recap(&mut app, thread_id);
|
||||
app.config.tui_auto_recap = false;
|
||||
|
||||
assert!(
|
||||
app.handle_generated_recap(request, temporary_thread_id, result)
|
||||
.is_none()
|
||||
);
|
||||
assert!(app.recap.in_flight_request.is_none());
|
||||
assert!(app.recap.scheduled_check.is_none());
|
||||
assert_eq!(app.recap.last_recapped_turn_count, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn obsolete_recap_result_does_not_clear_the_current_request() {
|
||||
let thread_id = ThreadId::new();
|
||||
|
||||
@@ -327,8 +327,7 @@ impl App {
|
||||
self.chat_widget.pause_unavailable_thread();
|
||||
self.chat_widget.add_info_message("This conversation is unavailable. Its cached transcript and draft remain here; input is paused. Open the agent picker or return to the parent to continue.".into(), /*hint*/ None);
|
||||
} else {
|
||||
self.recap
|
||||
.schedule_check(id, self.app_event_tx.clone(), Instant::now());
|
||||
self.schedule_recap_check(id, Instant::now());
|
||||
}
|
||||
} else {
|
||||
self.active_thread_id = None;
|
||||
|
||||
@@ -620,8 +620,7 @@ impl App {
|
||||
}
|
||||
let now = Instant::now();
|
||||
self.recap.seed_from_progress(recap_progress, now);
|
||||
self.recap
|
||||
.schedule_check(thread_id, self.app_event_tx.clone(), now);
|
||||
self.schedule_recap_check(thread_id, now);
|
||||
|
||||
self.render_thread_snapshot(tui, app_server, thread_id, snapshot, !is_replay_only)?;
|
||||
if is_replay_only {
|
||||
|
||||
@@ -180,8 +180,7 @@ async fn reconnect_restores_history_permissions_and_keeps_old_input_paused() ->
|
||||
app.recap
|
||||
.note_turn_finished(&TurnStatus::Completed, before_disconnect);
|
||||
}
|
||||
app.recap
|
||||
.schedule_check(id, app.app_event_tx.clone(), Instant::now());
|
||||
app.schedule_recap_check(id, Instant::now());
|
||||
let old_sender = app.app_event_tx.clone();
|
||||
let connected = reconnect(
|
||||
app.app_server_target.clone(),
|
||||
|
||||
@@ -191,8 +191,9 @@ stream_max_retries = 0
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn manual_recap_bypasses_automatic_eligibility_and_reports_failure() -> Result<()> {
|
||||
async fn manual_recap_works_when_auto_recap_disabled() -> Result<()> {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
app.config.tui_auto_recap = false;
|
||||
let thread_id = ThreadId::new();
|
||||
app.active_thread_id = Some(thread_id);
|
||||
app.transcript_cells
|
||||
@@ -205,6 +206,8 @@ async fn manual_recap_bypasses_automatic_eligibility_and_reports_failure() -> Re
|
||||
let (mut app_server, _requests, proxy) = start_recording_remote_app_server(&app.config).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
|
||||
// An automatic request must not occupy the slot needed by a manual request.
|
||||
app.request_recap(&app_server, thread_id, RecapTrigger::Automatic);
|
||||
app.handle_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
@@ -295,6 +298,72 @@ async fn manual_recap_bypasses_automatic_eligibility_and_reports_failure() -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_recap_opt_out_blocks_requests_and_cleans_up_pending_start() -> Result<()> {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let thread_id = ThreadId::new();
|
||||
prepare_eligible_recap(&mut app, thread_id);
|
||||
let (mut app_server, requests, proxy) = start_recording_remote_app_server(&app.config).await?;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
app.config.tui_auto_recap = false;
|
||||
app.schedule_recap_check(thread_id, Instant::now());
|
||||
app.request_recap(&app_server, thread_id, RecapTrigger::Automatic);
|
||||
app.handle_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
AppEvent::CheckRecap { thread_id },
|
||||
)
|
||||
.await?;
|
||||
tokio::task::yield_now().await;
|
||||
|
||||
assert!(app_event_rx.try_recv().is_err());
|
||||
assert_eq!(
|
||||
recorded_params(&requests, "thread/start"),
|
||||
Vec::<Value>::new()
|
||||
);
|
||||
let rendered = app
|
||||
.transcript_cells
|
||||
.iter()
|
||||
.flat_map(|cell| cell.display_lines(/*width*/ 80))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
app.config.tui_auto_recap = true;
|
||||
app.request_recap(&app_server, thread_id, RecapTrigger::Automatic);
|
||||
let started_event = tokio::time::timeout(Duration::from_secs(/*secs*/ 5), app_event_rx.recv())
|
||||
.await?
|
||||
.expect("recap start event");
|
||||
assert!(matches!(started_event, AppEvent::RecapStarted { .. }));
|
||||
app.config.tui_auto_recap = false;
|
||||
app.handle_event(&mut tui, &mut app_server, started_event)
|
||||
.await?;
|
||||
tokio::time::timeout(Duration::from_secs(/*secs*/ 5), async {
|
||||
while recorded_params(&requests, "thread/unsubscribe").is_empty() {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Finish the unsubscribe round trip before shutting down the recording proxy.
|
||||
app_server
|
||||
.thread_loaded_list(codex_app_server_protocol::ThreadLoadedListParams {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
recorded_params(&requests, "turn/start"),
|
||||
Vec::<Value>::new()
|
||||
);
|
||||
assert!(app.temporary_structured_requests.is_empty());
|
||||
app_server.shutdown().await?;
|
||||
proxy.await??;
|
||||
insta::assert_snapshot!(
|
||||
"auto_recap_disabled_transcript",
|
||||
lines_to_single_string(&rendered)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recap_generation_uses_remote_workspace_cwd() -> Result<()> {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: tui/src/app/tests/recap_generation_tests.rs
|
||||
expression: lines_to_single_string(&rendered)
|
||||
---
|
||||
|
||||
› Finish the recap implementation
|
||||
@@ -973,11 +973,10 @@ impl App {
|
||||
&& let ServerNotification::TurnCompleted(notification) = ¬ification
|
||||
{
|
||||
let now = Instant::now();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
|
||||
self.recap
|
||||
.note_turn_finished(¬ification.turn.status, now);
|
||||
self.recap.schedule_check(thread_id, app_event_tx, now);
|
||||
self.schedule_recap_check(thread_id, now);
|
||||
}
|
||||
let misalignment_policy_violation =
|
||||
match ¬ification {
|
||||
@@ -1340,10 +1339,9 @@ impl App {
|
||||
.send(AppEvent::BeginInitialHistoryReplayBuffer);
|
||||
}
|
||||
let now = Instant::now();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
|
||||
self.recap.seed_from_turns(&turns, now);
|
||||
self.recap.schedule_check(thread_id, app_event_tx, now);
|
||||
self.schedule_recap_check(thread_id, now);
|
||||
|
||||
self.chat_widget
|
||||
.replay_thread_turns(turns, ReplayKind::ResumeInitialMessages);
|
||||
|
||||
Reference in New Issue
Block a user