Skip to content

feat(/new): add new session command#2235

Merged
Hmbown merged 1 commit into
Hmbown:mainfrom
reidliu41:feat/new-session-command
May 27, 2026
Merged

feat(/new): add new session command#2235
Hmbown merged 1 commit into
Hmbown:mainfrom
reidliu41:feat/new-session-command

Conversation

@reidliu41
Copy link
Copy Markdown
Contributor

@reidliu41 reidliu41 commented May 26, 2026

Summary

Fixes #2234

Adds /new [--force] as an explicit way to start a fresh saved session from inside the TUI.

Previously, /clear could reset conversation state, but that made session lifecycle behavior ambiguous: clearing a
transcript is not the same user intent as starting a new saved session after resume. /new now gives that action its own command.

This change:

  • adds /new [--force] to the command registry, help, and completions
  • creates a distinct new session id and syncs an empty saved session
  • keeps previous sessions available through /resume
  • blocks switching when there is unsent input, queued work, an active turn, compaction, or running background tasks
  • allows explicit discard with /new --force
  • preserves /clear as a cleanup command

Testing

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test --workspace --all-features

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes

Greptile Summary

This PR introduces /new [--force] as a first-class TUI command for starting a fresh saved session, distinct from /clear (which only resets transcript state). It achieves this by extracting the shared state-reset logic from clear() into a new reset_conversation_state() helper in core.rs, then building new_session() on top of it with its own blocker guards, a pre-generated UUID session ID, and a SyncSession dispatch that persists an empty snapshot.

  • core.rs: Refactors clear() into clear() + reset_conversation_state(); the observable behaviour of /clear is unchanged.
  • session.rs: Adds new_session() with five blocker conditions (unsent input, queued messages, in-flight turn, compaction, running tasks), --force override, and four new unit tests; the session title and success message are hardcoded English strings rather than going through the locale system.
  • localization.rs: Adds CmdNewDescription to the enum, ALL_MESSAGE_IDS, and all five locale translation functions.

Confidence Score: 4/5

Safe to merge; the new command is additive, the /clear refactor is a pure extraction with no behavioural change, and the blocker logic is well-tested.

The refactor of clear() into reset_conversation_state() is low-risk and covered by existing tests. The new_session() logic is straightforward and has four targeted unit tests. The two findings — a hardcoded English session title/message that bypasses the locale system, and an undocumented 'force' shorthand — are cosmetic and do not affect correctness or data integrity.

crates/tui/src/commands/session.rs — the session title and confirmation message are hardcoded English and the undocumented 'force' shorthand is accepted; all other changed files are clean.

Important Files Changed

Filename Overview
crates/tui/src/commands/session.rs Adds new_session() with blocker guards, UUID-based ID creation, state reset, and SyncSession dispatch; session title and confirmation message are hardcoded English; "force" (without --) accepted as undocumented alias
crates/tui/src/commands/core.rs Refactors clear() by extracting shared state-reset logic into reset_conversation_state(); clear() behavior is functionally unchanged, all existing tests still cover the reset path
crates/tui/src/commands/mod.rs Registers /new command in COMMANDS registry and dispatch table; ordering and wiring look correct
crates/tui/src/localization.rs Adds CmdNewDescription to the MessageId enum, ALL_MESSAGE_IDS list, and all five locale translation functions (traditional_chinese delegates to chinese_simplified which is covered); no gaps

Sequence Diagram

sequenceDiagram
    participant User
    participant TUI as TUI (ui.rs)
    participant Cmd as new_session()
    participant Core as reset_conversation_state()
    participant Engine as EngineHandle
    participant Persist as persistence_actor

    User->>TUI: /new [--force]
    TUI->>Cmd: new_session(app, arg)
    alt !force
        Cmd->>Cmd: new_session_blockers(app)
        Note over Cmd: checks unsent input,<br/>queued msgs, is_loading,<br/>is_compacting, running tasks
        Cmd-->>TUI: CommandResult::error (if blocked)
    end
    Cmd->>Core: reset_conversation_state(app)
    Core-->>Cmd: todos_cleared (bool)
    Cmd->>Cmd: app.clear_input() / session_artifacts / context_references / tool_evidence cleared
    Cmd->>Cmd: "app.current_session_id = Some(new_uuid)"
    Cmd-->>TUI: "CommandResult::SyncSession{session_id: Some(new_uuid), messages: []}"
    TUI->>Engine: "Op::SyncSession{session_id: Some(new_uuid), messages: []}"
    TUI->>Persist: PersistRequest::SessionSnapshot (empty, new_uuid)
    TUI->>Persist: PersistRequest::ClearCheckpoint
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "feat: add new session command" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

  Add /new to start a fresh saved session from the TUI without overloading /clear.
  The command creates a distinct session id, resets conversation state, and keeps
  previous sessions available through /resume.

  Block unsafe switches when pending input or active work exists, with /new --force
  available for explicit discard.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new /new command to start a fresh saved session from the current TUI state, including support for a --force flag to bypass blockers. A critical issue was identified where using --force to bypass active background operations (such as an in-progress turn, context compaction, or running background tasks) could lead to state corruption when those operations eventually complete and write back to the App state. It is recommended to split the blockers into critical blockers (which cannot be bypassed) and discardable blockers (which can be bypassed with --force).

Comment on lines +150 to +203
if !force {
let blockers = new_session_blockers(app);
if !blockers.is_empty() {
return CommandResult::error(format!(
"Cannot start a new session while {}. Run `/new --force` to discard pending work and start a fresh session.",
blockers.join(", ")
));
}
}

let new_id = uuid::Uuid::new_v4().to_string();
super::core::reset_conversation_state(app);
app.clear_input();
app.session_artifacts.clear();
app.session_context_references.clear();
app.tool_evidence.clear();
app.current_session_id = Some(new_id.clone());
app.session_title = Some("New Session".to_string());
app.scroll_to_bottom();

CommandResult::with_message_and_action(
format!(
"Started new session {} (New Session). Previous sessions remain available via /resume.",
crate::session_manager::truncate_id(&new_id)
),
AppAction::SyncSession {
session_id: Some(new_id),
messages: Vec::new(),
system_prompt: None,
model: app.model.clone(),
workspace: app.workspace.clone(),
},
)
}

fn new_session_blockers(app: &App) -> Vec<&'static str> {
let mut blockers = Vec::new();
if !app.input.trim().is_empty() {
blockers.push("the composer has unsent text");
}
if !app.queued_messages.is_empty() || app.queued_draft.is_some() {
blockers.push("queued messages are pending");
}
if app.is_loading || app.runtime_turn_status.as_deref() == Some("in_progress") {
blockers.push("a turn is in progress");
}
if app.is_compacting {
blockers.push("context compaction is running");
}
if app.task_panel.iter().any(|task| task.status == "running") {
blockers.push("background tasks are running");
}
blockers
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Critical vs Discardable Blockers

Allowing --force to bypass active operations like an in-progress turn or running context compaction is highly dangerous. When these background operations eventually complete, they will write back to the App state (e.g., appending messages or updating compaction results), which will corrupt the newly started session.

We should split the blockers into:

  1. Critical Blockers (active turn, compaction, running background tasks) which cannot be bypassed even with --force.
  2. Discardable Blockers (unsent composer text, queued messages) which can be safely discarded with --force.
    let critical_blockers = new_session_critical_blockers(app);
    if !critical_blockers.is_empty() {
        return CommandResult::error(format!(
            "Cannot start a new session while {}. Please wait for these operations to complete.",
            critical_blockers.join(", ")
        ));
    }

    if !force {
        let discardable_blockers = new_session_discardable_blockers(app);
        if !discardable_blockers.is_empty() {
            return CommandResult::error(format!(
                "Cannot start a new session while {}. Run `/new --force` to discard pending work and start a fresh session.",
                discardable_blockers.join(", ")
            ));
        }
    }

    let new_id = uuid::Uuid::new_v4().to_string();
    super::core::reset_conversation_state(app);
    app.clear_input();
    app.session_artifacts.clear();
    app.session_context_references.clear();
    app.tool_evidence.clear();
    app.current_session_id = Some(new_id.clone());
    app.session_title = Some("New Session".to_string());
    app.scroll_to_bottom();

    CommandResult::with_message_and_action(
        format!(
            "Started new session {} (New Session). Previous sessions remain available via /resume.",
            crate::session_manager::truncate_id(&new_id)
        ),
        AppAction::SyncSession {
            session_id: Some(new_id),
            messages: Vec::new(),
            system_prompt: None,
            model: app.model.clone(),
            workspace: app.workspace.clone(),
        },
    )
}

fn new_session_critical_blockers(app: &App) -> Vec<&'static str> {
    let mut blockers = Vec::new();
    if app.is_loading || app.runtime_turn_status.as_deref() == Some("in_progress") {
        blockers.push("a turn is in progress");
    }
    if app.is_compacting {
        blockers.push("context compaction is running");
    }
    if app.task_panel.iter().any(|task| task.status == "running") {
        blockers.push("background tasks are running");
    }
    blockers
}

fn new_session_discardable_blockers(app: &App) -> Vec<&'static str> {
    let mut blockers = Vec::new();
    if !app.input.trim().is_empty() {
        blockers.push("the composer has unsent text");
    }
    if !app.queued_messages.is_empty() || app.queued_draft.is_some() {
        blockers.push("queued messages are pending");
    }
    blockers
}

Comment on lines +166 to +174
app.current_session_id = Some(new_id.clone());
app.session_title = Some("New Session".to_string());
app.scroll_to_bottom();

CommandResult::with_message_and_action(
format!(
"Started new session {} (New Session). Previous sessions remain available via /resume.",
crate::session_manager::truncate_id(&new_id)
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The session title "New Session" and the full success message are hardcoded English strings that bypass the locale system. Every other command-level user-visible string goes through tr(locale, MessageId::...). A Japanese or Simplified-Chinese user who runs /new will see English text in both the status bar and the sessions picker. Adding MessageId::NewSessionTitle and MessageId::NewSessionConfirm (analogous to ClearConversation) and wiring them through localization.rs would make this consistent.

Suggested change
app.current_session_id = Some(new_id.clone());
app.session_title = Some("New Session".to_string());
app.scroll_to_bottom();
CommandResult::with_message_and_action(
format!(
"Started new session {} (New Session). Previous sessions remain available via /resume.",
crate::session_manager::truncate_id(&new_id)
),
app.current_session_id = Some(new_id.clone());
let locale = app.ui_locale;
app.session_title = Some(tr(locale, MessageId::NewSessionTitle).to_string());
app.scroll_to_bottom();
CommandResult::with_message_and_action(
tr(locale, MessageId::NewSessionConfirm)
.replace("{id}", &crate::session_manager::truncate_id(&new_id).to_string()),

Fix in Codex Fix in Claude Code Fix in Cursor

pub fn new_session(app: &mut App, arg: Option<&str>) -> CommandResult {
let force = match arg.map(str::trim).filter(|s| !s.is_empty()) {
None => false,
Some("--force" | "force") => true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The pattern match accepts "force" (without the -- prefix) as a valid flag, but the help text and usage string only document --force. A user who accidentally types /new force expecting an error will silently discard pending work. Accepting only the documented form avoids surprising behaviour.

Suggested change
Some("--force" | "force") => true,
Some("--force") => true,

Fix in Codex Fix in Claude Code Fix in Cursor

@reidliu41
Copy link
Copy Markdown
Contributor Author

hi @Hmbown please take a look when you have time. thanks.

@Hmbown Hmbown merged commit 54151a4 into Hmbown:main May 27, 2026
9 checks passed
Hmbown added a commit that referenced this pull request May 27, 2026
- Merged #2235 (/new session command from @reidliu41, all CI green)
- Added Xiaomi MiMo to provider examples and env-var table
- Added verification gate paragraph in harness section
- Updated @reidliu41 entries with #2235 across all READMEs
- CHANGELOG: added /new command entry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add /new command to start a fresh session from resumed state

2 participants