feat: Support persisting paused todo lists to sessions so users can exit and resume paused todo sessions; Also fixed a bug so todo list pausing is only supported when being used in a session since it typically requires multi-turn communication

This commit is contained in:
Dark-Alex-17 committed 2026-09-14 13:57:08 -06:00
1 parent 5d59adadbc
commit 77da48a40f
7 files changed
+473 -33

No files matched your search

+63 -17
View File
@@ -52,7 +52,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
1. BEFORE marking a todo done: verify the work compiles/works. No premature completion.
2. If a todo is broad (e.g. \"implement X and implement Y\"): break it into specific subtasks FIRST using todo__add, then work on those.\n\
3. Each todo should be atomic and be \"single responsibility\" - completable in one focused action.
4. If you are blocked on user input or a user-side action: call todo__pause with the reason, ask the user, and end your turn. NEVER mark unfinished todos done just to stop this reminder.
4. If you are blocked on user input or a user-side action: call todo__pause with the reason (if available); otherwise ask the user directly in your reply and end your turn. NEVER mark unfinished todos done just to stop this reminder.
5. Otherwise, continue with the next pending item now. Call tools immediately."
};
@@ -433,6 +433,8 @@ Type ".help" for additional help.
// OSC 11 / DA1 responses) so they don't get injected into the prompt.
drain_stale_tty_input();
print_pause_banner(&self.ctx.read());
loop {
if self.abort_signal.aborted_ctrld() {
break;
@@ -927,6 +929,7 @@ pub async fn run_repl_command(
let (compressed, active) = replay::snapshot(session);
replay::render(app.as_ref(), &compressed, &active)?;
}
print_pause_banner(ctx);
}
".install" => match parse_repl_install(args) {
ReplInstallDispatch::Builtins(category) => config::install_assets(category)?,
@@ -983,6 +986,7 @@ pub async fn run_repl_command(
if let Some(session) = &ctx.session {
let (compressed, active) = replay::snapshot(session);
replay::render(app.as_ref(), &compressed, &active)?;
print_pause_banner(ctx);
}
}
None => {
@@ -1538,22 +1542,7 @@ async fn ask(
}
reset_continuation(ctx);
if let Some(reason) = ctx.auto_continue_paused.clone()
&& ctx.todo_list.has_incomplete()
{
let remaining = ctx.todo_list.incomplete_count();
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!(
"\n⏸ {}",
color.italic().paint(format!(
"Auto-continue paused, awaiting user ({remaining} incomplete todo(s) preserved): {reason}"
))
);
}
print_pause_banner(ctx);
if ctx.maybe_autoname_session() {
let color = if app.light_theme() {
nu_ansi_term::Color::LightGray
@@ -1656,6 +1645,28 @@ fn reset_continuation(ctx: &mut RequestContext) {
ctx.reset_continuation_count();
}
fn pause_banner_text(ctx: &RequestContext) -> Option<String> {
let reason = ctx.auto_continue_paused.as_deref()?;
if !ctx.todo_list.has_incomplete() {
return None;
}
let remaining = ctx.todo_list.incomplete_count();
Some(format!(
"Auto-continue paused, awaiting user ({remaining} incomplete todo(s) preserved): {reason}"
))
}
fn print_pause_banner(ctx: &RequestContext) {
if let Some(text) = pause_banner_text(ctx) {
let color = if ctx.app.config.light_theme() {
nu_ansi_term::Color::LightGray
} else {
nu_ansi_term::Color::DarkGray
};
eprintln!("\n⏸ {}", color.italic().paint(text));
}
}
fn unknown_command() -> Result<()> {
bail!(r#"Unknown command. Type ".help" for additional help."#);
}
@@ -2468,4 +2479,39 @@ mod tests {
let result = MULTILINE_RE.captures(input).unwrap();
assert!(result.is_none());
}
fn banner_ctx() -> RequestContext {
use crate::config::{AppState, WorkingMode};
RequestContext::new(Arc::new(AppState::test_default()), WorkingMode::Cmd)
}
#[test]
fn pause_banner_text_paused_with_incomplete_todos() {
let mut ctx = banner_ctx();
ctx.add_todo("write code");
ctx.add_todo("run tests");
ctx.pause_auto_continue("Which database?");
let text = pause_banner_text(&ctx).unwrap();
assert!(text.contains("Which database?"), "{text}");
assert!(text.contains("2 incomplete todo(s) preserved"), "{text}");
}
#[test]
fn pause_banner_text_none_when_all_todos_complete() {
let mut ctx = banner_ctx();
let id = ctx.add_todo("write code");
ctx.mark_todo_done(id);
ctx.pause_auto_continue("Which database?");
assert_eq!(pause_banner_text(&ctx), None);
}
#[test]
fn pause_banner_text_none_when_unpaused() {
let mut ctx = banner_ctx();
ctx.add_todo("write code");
assert_eq!(pause_banner_text(&ctx), None);
}
}