use super::archive::workspace_archive_label;
use super::*;
use crate::agent::runloop::git::{
DirtyWorktreeStatus, git_dirty_worktree_entries, workspace_relative_display,
};
use crate::agent::runloop::unified::overlay_prompt::{OverlayWaitOutcome, show_overlay_and_wait};
use crate::agent::runloop::welcome::SessionBootstrap;
use std::sync::Arc;
use vtcode_tui::app::{
InlineHandle, InlineListItem, InlineListSelection, InlineSession, ListOverlayRequest,
TransientRequest, TransientSubmission,
};
const STARTUP_PLAN_MODE_ENTER_ACTION: &str = "plan_mode:start_enter";
const STARTUP_PLAN_MODE_STAY_ACTION: &str = "plan_mode:start_stay";
#[derive(Clone)]
pub(super) struct TurnHistoryCheckpoint {
pub(super) baseline_len: usize,
#[cfg(debug_assertions)]
prefix_fingerprint: u64,
}
impl TurnHistoryCheckpoint {
pub(super) fn capture(history: &[vtcode_core::llm::provider::Message]) -> Self {
Self {
baseline_len: history.len(),
#[cfg(debug_assertions)]
prefix_fingerprint: Self::prefix_fingerprint(history),
}
}
pub(super) fn rollback(&self, history: &mut Vec<vtcode_core::llm::provider::Message>) {
#[cfg(debug_assertions)]
self.assert_append_only(history);
history.truncate(self.baseline_len);
}
#[cfg(debug_assertions)]
fn assert_append_only(&self, history: &[vtcode_core::llm::provider::Message]) {
debug_assert!(
history.len() >= self.baseline_len,
"turn history rollback requires append-only growth after checkpoint"
);
debug_assert_eq!(
Self::prefix_fingerprint(&history[..self.baseline_len]),
self.prefix_fingerprint,
"turn history rollback requires the pre-checkpoint prefix to remain unchanged"
);
}
#[cfg(debug_assertions)]
fn prefix_fingerprint(history: &[vtcode_core::llm::provider::Message]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
serde_json::to_string(history)
.unwrap_or_default()
.hash(&mut hasher);
hasher.finish()
}
}
#[derive(Clone)]
pub(super) struct PendingTimeoutRecovery {
pub(super) reason: String,
pub(super) mode: RecoveryMode,
}
pub(super) fn remove_transient_system_notes(
history: &mut Vec<vtcode_core::llm::provider::Message>,
notes: &[String],
) {
for note in notes.iter().rev() {
if let Some(index) = history.iter().rposition(|message| {
message.role == vtcode_core::llm::provider::MessageRole::System
&& message.content.as_text() == note.as_str()
}) {
let _ = history.remove(index);
}
}
}
pub(super) fn build_tracked_file_freshness_note(
workspace: &std::path::Path,
stale_paths: &[std::path::PathBuf],
) -> Option<String> {
if stale_paths.is_empty() {
return None;
}
let display_paths = stale_paths
.iter()
.map(|path| format!("- {}", workspace_relative_display(workspace, path)))
.collect::<Vec<_>>()
.join("\n");
Some(format!(
"Freshness note: the following files changed on disk after VT Code last read them:\n{display_paths}\nRe-read these files before relying on earlier content because disk content is newer than the agent's prior read snapshot."
))
}
pub(super) fn build_unrelated_dirty_worktree_note(
workspace: &std::path::Path,
agent_touched_paths: &std::collections::BTreeSet<std::path::PathBuf>,
) -> Result<Option<String>> {
let Some(entries) = git_dirty_worktree_entries(workspace)? else {
return Ok(None);
};
let display_paths = entries
.into_iter()
.filter(|entry| {
entry.status == DirtyWorktreeStatus::Modified
&& !agent_touched_paths.contains(&entry.path)
})
.map(|entry| format!("- {}", workspace_relative_display(workspace, &entry.path)))
.collect::<Vec<_>>();
if display_paths.is_empty() {
return Ok(None);
}
Ok(Some(format!(
"Workspace note: the following files already have unrelated user modifications before this turn:\n{}\nTreat these files as user-owned changes. Do not edit, format, revert, or overwrite them unless the user explicitly asks to work on those files.",
display_paths.join("\n")
)))
}
pub(super) fn append_transient_turn_notes(
history: &mut Vec<vtcode_core::llm::provider::Message>,
workspace: &std::path::Path,
tool_registry: &vtcode_core::tools::registry::ToolRegistry,
agent_touched_paths: &std::collections::BTreeSet<std::path::PathBuf>,
) -> Vec<String> {
let mut transient_system_notes = Vec::with_capacity(2);
if let Some(note) = {
let stale_paths = tool_registry
.edited_file_monitor_ref()
.stale_tracked_paths();
build_tracked_file_freshness_note(workspace, &stale_paths)
} {
transient_system_notes.push(note.clone());
history.push(vtcode_core::llm::provider::Message::system(note));
}
match build_unrelated_dirty_worktree_note(workspace, agent_touched_paths) {
Ok(Some(note)) => {
transient_system_notes.push(note.clone());
history.push(vtcode_core::llm::provider::Message::system(note));
}
Ok(None) => {}
Err(err) => {
tracing::warn!(
error = %err,
"Failed to inspect unrelated dirty worktree entries before turn"
);
}
}
transient_system_notes
}
pub(super) fn latest_assistant_result_text(
messages: &[vtcode_core::llm::provider::Message],
) -> Option<String> {
messages
.iter()
.rev()
.find(|message| message.role == vtcode_core::llm::provider::MessageRole::Assistant)
.map(|message| message.content.as_text().trim().to_string())
.filter(|text| !text.is_empty())
}
pub(super) fn take_pending_resumed_user_prompt(
history: &mut Vec<vtcode_core::llm::provider::Message>,
) -> Option<String> {
let user_index = history
.iter()
.rposition(|message| message.role == vtcode_core::llm::provider::MessageRole::User)?;
if history
.iter()
.skip(user_index + 1)
.any(|message| message.role != vtcode_core::llm::provider::MessageRole::System)
{
return None;
}
let prompt = history[user_index].content.as_text().trim().to_string();
if prompt.is_empty() {
return None;
}
let _ = history.remove(user_index);
Some(prompt)
}
pub(super) fn live_reload_preserves_session_config(
initial_vt_cfg: Option<&VTCodeConfig>,
runtime_cfg: &CoreAgentConfig,
) -> bool {
let Some(initial_vt_cfg) = initial_vt_cfg else {
return true;
};
let mut reloaded_vt_cfg =
vtcode_core::config::loader::ConfigManager::load_from_workspace(&runtime_cfg.workspace)
.ok()
.map(|manager| manager.config().clone());
crate::agent::agents::apply_runtime_overrides(reloaded_vt_cfg.as_mut(), runtime_cfg);
let Some(reloaded_vt_cfg) = reloaded_vt_cfg else {
return false;
};
let Ok(initial_value) = serde_json::to_value(initial_vt_cfg) else {
return false;
};
let Ok(reloaded_value) = serde_json::to_value(reloaded_vt_cfg) else {
return false;
};
initial_value == reloaded_value
}
pub(super) fn prepare_resume_bootstrap_without_archive(
resume: &ResumeSession,
mut metadata: vtcode_core::utils::session_archive::SessionArchiveMetadata,
reserved_archive_id: Option<String>,
) -> (vtcode_core::core::threads::ThreadBootstrap, String) {
let source_metadata = &resume.snapshot().metadata;
let is_compatible = metadata.workspace_path == source_metadata.workspace_path
&& metadata.provider == source_metadata.provider
&& metadata.model == source_metadata.model;
if is_compatible && let Some(lineage_id) = source_metadata.prompt_cache_lineage_id.as_ref() {
metadata.prompt_cache_lineage_id = Some(lineage_id.clone());
}
metadata.continuation_metadata = source_metadata.continuation_metadata.clone();
if resume.is_fork() {
metadata.parent_session_id = Some(resume.identifier());
metadata.fork_mode = Some(if resume.summarize_fork() {
vtcode_core::utils::session_archive::SessionForkMode::Summarized
} else {
vtcode_core::utils::session_archive::SessionForkMode::FullCopy
});
}
let mut bootstrap = resume.bootstrap().clone();
bootstrap.metadata = Some(metadata);
if resume.is_fork() {
bootstrap.archive_listing = None;
}
let thread_id = match resume.intent() {
vtcode_core::core::threads::ArchivedSessionIntent::ResumeInPlace => resume.identifier(),
vtcode_core::core::threads::ArchivedSessionIntent::ForkNewArchive { .. } => {
reserved_archive_id.unwrap_or_else(|| {
vtcode_core::utils::session_archive::generate_session_archive_identifier(
&workspace_archive_label(std::path::Path::new(
&resume.snapshot().metadata.workspace_path,
)),
resume.custom_suffix().map(str::to_owned),
)
})
}
};
(bootstrap, thread_id)
}
pub(super) async fn checkpoint_session_archive_start(
archive: &vtcode_core::utils::session_archive::SessionArchive,
thread_handle: &vtcode_core::core::threads::ThreadRuntimeHandle,
) -> Result<()> {
let snapshot = thread_handle.snapshot();
let recent_messages = snapshot.messages.iter().map(SessionMessage::from).collect();
archive
.persist_progress_async(SessionProgressArgs {
total_messages: snapshot.messages.len(),
distinct_tools: Vec::new(),
recent_messages,
turn_number: 1,
token_usage: None,
max_context_tokens: None,
loaded_skills: Some(snapshot.loaded_skills),
})
.await?;
Ok(())
}
pub(super) async fn force_reload_workspace_config_for_execution(
workspace: &std::path::Path,
runtime_cfg: &CoreAgentConfig,
vt_cfg: &mut Option<VTCodeConfig>,
tool_registry: &mut vtcode_core::tools::registry::ToolRegistry,
async_mcp_manager: Option<&crate::agent::runloop::unified::async_mcp_manager::AsyncMcpManager>,
) -> Result<()> {
crate::agent::runloop::unified::turn::workspace::refresh_vt_config(
workspace,
runtime_cfg,
vt_cfg,
)
.await?;
if let Some(cfg) = vt_cfg.as_ref() {
crate::agent::runloop::unified::turn::workspace::apply_workspace_config_to_registry(
tool_registry,
cfg,
)?;
if let Some(mcp_manager) = async_mcp_manager {
let desired_policy = crate::agent::runloop::unified::async_mcp_manager::approval_policy_from_human_in_the_loop(
cfg.security.human_in_the_loop,
);
if mcp_manager.approval_policy() != desired_policy {
mcp_manager.set_approval_policy(desired_policy);
}
}
}
Ok(())
}
pub(super) struct ExitHeaderDisplay {
pub(super) provider_label: String,
pub(super) reasoning_label: String,
pub(super) context_window_size: usize,
pub(super) mode_label: String,
pub(super) editing_mode: vtcode_tui::app::EditingMode,
pub(super) autonomous_mode: bool,
pub(super) full_auto: bool,
}
pub(super) fn build_exit_header_context_fast(
config: &CoreAgentConfig,
session_bootstrap: &SessionBootstrap,
display: ExitHeaderDisplay,
) -> vtcode_tui::app::InlineHeaderContext {
use vtcode_core::config::constants::ui;
let trust_label = match session_bootstrap.acp_workspace_trust {
Some(vtcode_core::config::AgentClientProtocolZedWorkspaceTrustMode::FullAuto) => {
"full_auto"
}
Some(vtcode_core::config::AgentClientProtocolZedWorkspaceTrustMode::ToolsPolicy) => {
"tools_policy"
}
None if display.full_auto => "full auto",
None => "tools policy",
};
vtcode_tui::app::InlineHeaderContext {
app_name: vtcode_core::config::constants::app::DISPLAY_NAME.to_string(),
provider: format!("{}{}", ui::HEADER_PROVIDER_PREFIX, display.provider_label),
model: format!("{}{}", ui::HEADER_MODEL_PREFIX, config.model),
context_window_size: Some(display.context_window_size),
version: env!("CARGO_PKG_VERSION").to_string(),
search_tools: Some(crate::agent::runloop::ui::build_search_tools_badge(
&config.workspace,
)),
persistent_memory: None,
pr_review: None,
editor_context: None,
git: String::new(),
mode: display.mode_label,
reasoning: format!("{}{}", ui::HEADER_REASONING_PREFIX, display.reasoning_label),
reasoning_stage: None,
workspace_trust: format!("{}{}", ui::HEADER_TRUST_PREFIX, trust_label),
tools: String::new(),
mcp: format!(
"{}{}",
ui::HEADER_MCP_PREFIX,
ui::HEADER_UNKNOWN_PLACEHOLDER
),
highlights: Vec::new(),
subagent_badges: Vec::new(),
editing_mode: display.editing_mode,
autonomous_mode: display.autonomous_mode,
}
}
pub(super) async fn prompt_startup_plan_mode(
handle: &InlineHandle,
session: &mut InlineSession,
ctrl_c_state: &Arc<crate::agent::runloop::unified::state::CtrlCState>,
ctrl_c_notify: &Arc<Notify>,
) -> Result<bool> {
let overlay = TransientRequest::List(ListOverlayRequest {
title: "Enter Plan Mode?".to_string(),
lines: vec![
"Your configuration sets default editing mode to Plan.".to_string(),
"Plan Mode is read-only and blocks mutating tools.".to_string(),
],
footer_hint: Some("You can toggle later with `/plan`.".to_string()),
items: vec![
InlineListItem {
title: "Enter Plan Mode".to_string(),
subtitle: Some("Switch to read-only planning.".to_string()),
badge: Some("Recommended".to_string()),
indent: 0,
selection: Some(InlineListSelection::ConfigAction(
STARTUP_PLAN_MODE_ENTER_ACTION.to_string(),
)),
search_value: None,
},
InlineListItem {
title: "Stay in Edit Mode".to_string(),
subtitle: Some("Continue in edit mode.".to_string()),
badge: None,
indent: 0,
selection: Some(InlineListSelection::ConfigAction(
STARTUP_PLAN_MODE_STAY_ACTION.to_string(),
)),
search_value: None,
},
],
selected: Some(InlineListSelection::ConfigAction(
STARTUP_PLAN_MODE_ENTER_ACTION.to_string(),
)),
search: None,
hotkeys: Vec::new(),
});
let outcome = show_overlay_and_wait(
handle,
session,
overlay,
ctrl_c_state,
ctrl_c_notify,
|submission| match submission {
TransientSubmission::Selection(InlineListSelection::ConfigAction(action))
if action == STARTUP_PLAN_MODE_ENTER_ACTION =>
{
Some(true)
}
TransientSubmission::Selection(InlineListSelection::ConfigAction(action))
if action == STARTUP_PLAN_MODE_STAY_ACTION =>
{
Some(false)
}
TransientSubmission::Selection(_) => Some(false),
_ => None,
},
)
.await?;
Ok(matches!(outcome, OverlayWaitOutcome::Submitted(true)))
}