fix(tui): sync runtime state on workspace switch

This commit is contained in:
J3y0r
2026-05-08 02:33:43 +00:00
parent f1d86901da
commit 598822bf10
4 changed files with 142 additions and 25 deletions
+34 -3
View File
@@ -701,6 +701,7 @@ pub type SharedTaskManager = Arc<TaskManager>;
pub struct TaskManager {
cfg: TaskManagerConfig,
default_workspace: Mutex<PathBuf>,
executor: Arc<dyn TaskExecutor>,
tasks_dir: PathBuf,
artifacts_dir: PathBuf,
@@ -766,8 +767,10 @@ impl TaskManager {
let (tasks, queue) = load_state(&tasks_dir, &queue_path)?;
let cancel_token = CancellationToken::new();
let default_workspace = cfg.default_workspace.clone();
let manager = Arc::new(Self {
cfg,
default_workspace: Mutex::new(default_workspace),
executor,
tasks_dir,
artifacts_dir,
@@ -810,6 +813,15 @@ impl TaskManager {
self.cancel_token.is_cancelled()
}
pub async fn set_default_workspace(&self, workspace: PathBuf) {
let mut default_workspace = self.default_workspace.lock().await;
*default_workspace = workspace;
}
pub async fn default_workspace(&self) -> PathBuf {
self.default_workspace.lock().await.clone()
}
/// Enqueue a new task.
pub async fn add_task(&self, req: NewTaskRequest) -> Result<TaskRecord> {
let prompt = req.prompt.trim().to_string();
@@ -822,9 +834,10 @@ impl TaskManager {
id: format!("task_{}", &Uuid::new_v4().to_string()[..8]),
prompt,
model: req.model.unwrap_or_else(|| self.cfg.default_model.clone()),
workspace: req
.workspace
.unwrap_or_else(|| self.cfg.default_workspace.clone()),
workspace: match req.workspace {
Some(workspace) => workspace,
None => self.default_workspace().await,
},
mode: req.mode.unwrap_or_else(|| self.cfg.default_mode.clone()),
allow_shell: req.allow_shell.unwrap_or(self.cfg.allow_shell),
trust_mode: req.trust_mode.unwrap_or(self.cfg.trust_mode),
@@ -1763,6 +1776,24 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn default_workspace_updates_for_future_tasks() -> Result<()> {
let root = std::env::temp_dir().join(format!("deepseek-task-test-{}", Uuid::new_v4()));
let new_workspace =
std::env::temp_dir().join(format!("deepseek-workspace-{}", Uuid::new_v4()));
let manager =
TaskManager::start_with_executor(test_config(root), Arc::new(MockExecutor)).await?;
manager.set_default_workspace(new_workspace.clone()).await;
let task = manager
.add_task(NewTaskRequest::from_prompt("test workspace default"))
.await?;
assert_eq!(manager.default_workspace().await, new_workspace);
assert_eq!(task.workspace, new_workspace);
Ok(())
}
#[tokio::test]
async fn record_tool_metadata_updates_explicit_task() -> Result<()> {
let root = std::env::temp_dir().join(format!("deepseek-task-test-{}", Uuid::new_v4()));
+5
View File
@@ -584,6 +584,11 @@ impl ShellManager {
self.sandbox_manager.is_available()
}
#[allow(dead_code)]
pub fn default_workspace(&self) -> &Path {
&self.default_workspace
}
/// Execute a shell command with the configured sandbox policy.
#[allow(dead_code)]
pub fn execute(
+31 -20
View File
@@ -38,7 +38,7 @@ use crate::core::coherence::CoherenceState;
use crate::core::engine::{EngineConfig, EngineHandle, spawn_engine};
use crate::core::events::Event as EngineEvent;
use crate::core::ops::Op;
use crate::hooks::HookEvent;
use crate::hooks::{HookEvent, HookExecutor};
use crate::models::{ContentBlock, Message, SystemPrompt, context_window_for_model};
use crate::palette;
use crate::prompts;
@@ -270,7 +270,7 @@ pub async fn run_tui(config: &Config, options: TuiOptions) -> Result<()> {
match load_result {
Ok(Some(saved)) => {
let recovered = apply_loaded_session(&mut app, &saved);
let recovered = apply_loaded_session(&mut app, config, &saved);
if !recovered {
app.status_message = Some(format!(
"Resumed session: {}",
@@ -4557,7 +4557,7 @@ async fn apply_command_result(
handle_mcp_ui_action(app, config, action).await;
}
AppAction::SwitchWorkspace { workspace } => {
switch_workspace(app, engine_handle, config, workspace).await;
switch_workspace(app, engine_handle, task_manager, config, workspace).await;
}
AppAction::SwitchProfile { profile } => {
app.config_profile = Some(profile.clone());
@@ -4626,9 +4626,31 @@ async fn apply_command_result(
Ok(false)
}
fn apply_workspace_runtime_state(app: &mut App, config: &Config, workspace: PathBuf) {
app.workspace = workspace.clone();
app.hooks = HookExecutor::new(config.hooks_config(), workspace.clone());
app.skills_dir = crate::tui::app::resolve_skills_dir(&workspace, &config.skills_dir(), config);
app.refresh_skill_cache();
app.workspace_context = None;
if let Ok(mut cell) = app.workspace_context_cell.lock() {
*cell = None;
}
app.workspace_context_refreshed_at = None;
app.file_tree = None;
let shell_manager = crate::tools::shell::new_shared_shell_manager(workspace);
app.runtime_services.shell_manager = Some(shell_manager);
app.runtime_services.hook_executor = Some(std::sync::Arc::new(app.hooks.clone()));
}
async fn sync_runtime_workspace_state(task_manager: &SharedTaskManager, workspace: PathBuf) {
task_manager.set_default_workspace(workspace).await;
}
async fn switch_workspace(
app: &mut App,
engine_handle: &mut EngineHandle,
task_manager: &SharedTaskManager,
config: &Config,
workspace: PathBuf,
) {
@@ -4646,20 +4668,8 @@ async fn switch_workspace(
return;
}
app.workspace = workspace.clone();
app.hooks = HookExecutor::new(config.hooks_config(), workspace.clone());
app.skills_dir = crate::tui::app::resolve_skills_dir(&workspace, &config.skills_dir(), config);
app.refresh_skill_cache();
app.workspace_context = None;
if let Ok(mut cell) = app.workspace_context_cell.lock() {
*cell = None;
}
app.workspace_context_refreshed_at = None;
app.file_tree = None;
let shell_manager = crate::tools::shell::new_shared_shell_manager(workspace.clone());
app.runtime_services.shell_manager = Some(shell_manager);
app.runtime_services.hook_executor = Some(std::sync::Arc::new(app.hooks.clone()));
apply_workspace_runtime_state(app, config, workspace.clone());
sync_runtime_workspace_state(task_manager, workspace.clone()).await;
let _ = engine_handle.send(Op::Shutdown).await;
let engine_config = build_engine_config(app, config);
@@ -5564,7 +5574,8 @@ async fn handle_view_events(
match manager.load_session(&session_id) {
Ok(session) => {
let recovered = apply_loaded_session(app, &session);
let recovered = apply_loaded_session(app, config, &session);
sync_runtime_workspace_state(task_manager, app.workspace.clone()).await;
let _ = engine_handle
.send(Op::SyncSession {
messages: app.api_messages.clone(),
@@ -5888,7 +5899,7 @@ async fn apply_provider_picker_api_key(
switch_provider(app, engine_handle, config, provider, None).await;
}
fn apply_loaded_session(app: &mut App, session: &SavedSession) -> bool {
fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession) -> bool {
let (messages, recovered_draft) = recover_interrupted_user_tail(&session.messages);
app.api_messages = messages;
app.clear_history();
@@ -5934,7 +5945,7 @@ fn apply_loaded_session(app: &mut App, session: &SavedSession) -> bool {
app.viewport.transcript_selection.clear();
app.model.clone_from(&session.metadata.model);
app.update_model_compaction_budget();
app.workspace.clone_from(&session.metadata.workspace);
apply_workspace_runtime_state(app, config, session.metadata.workspace.clone());
app.session.total_tokens = u32::try_from(session.metadata.total_tokens).unwrap_or(u32::MAX);
app.session.total_conversation_tokens = app.session.total_tokens;
app.session.session_cost = 0.0;
+72 -2
View File
@@ -586,7 +586,7 @@ fn apply_loaded_session_restores_dangling_user_tail_as_retry_draft() {
"finish the Qthresh proof bundle",
)]);
let recovered = apply_loaded_session(&mut app, &session);
let recovered = apply_loaded_session(&mut app, &Config::default(), &session);
assert!(recovered);
assert!(app.api_messages.is_empty());
@@ -637,7 +637,7 @@ fn apply_loaded_session_resets_unpersisted_telemetry() {
let mut session = saved_session_with_messages(vec![text_message("assistant", "ready")]);
session.metadata.total_tokens = 500;
let recovered = apply_loaded_session(&mut app, &session);
let recovered = apply_loaded_session(&mut app, &Config::default(), &session);
assert!(!recovered);
assert_eq!(app.session.total_tokens, 500);
@@ -657,6 +657,76 @@ fn apply_loaded_session_resets_unpersisted_telemetry() {
assert!(app.session.turn_cache_history.is_empty());
}
#[test]
fn apply_loaded_session_resets_workspace_runtime_state() {
let mut app = create_test_app();
let config = Config::default();
let old_shell_manager = app
.runtime_services
.shell_manager
.as_ref()
.expect("shell manager")
.clone();
let old_context_cell = app.workspace_context_cell.clone();
app.workspace_context = Some(Workspace::new(PathBuf::from(".")));
if let Ok(mut cell) = old_context_cell.lock() {
*cell = Some("old workspace context".to_string());
}
app.workspace_context_refreshed_at = Some(Instant::now());
app.file_tree = Some(crate::tui::file_tree::FileTreeState::new(PathBuf::from(
".",
)));
let mut session = saved_session_with_messages(vec![text_message("assistant", "ready")]);
session.metadata.workspace = TempDir::new().expect("temp dir").path().to_path_buf();
let recovered = apply_loaded_session(&mut app, &config, &session);
assert!(!recovered);
assert_eq!(app.workspace, session.metadata.workspace);
assert!(app.workspace_context.is_none());
assert!(app.workspace_context_refreshed_at.is_none());
assert!(app.file_tree.is_none());
assert!(old_context_cell.lock().expect("context cell").is_none());
let new_shell_manager = app
.runtime_services
.shell_manager
.as_ref()
.expect("shell manager")
.clone();
assert!(!std::sync::Arc::ptr_eq(
&old_shell_manager,
&new_shell_manager
));
assert_eq!(
new_shell_manager
.lock()
.expect("shell manager")
.default_workspace(),
session.metadata.workspace.as_path()
);
assert!(app.runtime_services.hook_executor.is_some());
}
#[test]
fn apply_loaded_session_updates_current_workspace_display() {
let mut app = create_test_app();
let config = Config::default();
let workspace = TempDir::new().expect("temp dir");
let mut session = saved_session_with_messages(vec![text_message("assistant", "ready")]);
session.metadata.workspace = workspace.path().to_path_buf();
let recovered = apply_loaded_session(&mut app, &config, &session);
let result = commands::core::workspace_switch(&mut app, None);
assert!(!recovered);
assert_eq!(
result.message,
Some(format!("Current workspace: {}", workspace.path().display()))
);
assert!(result.action.is_none());
}
#[tokio::test]
async fn drain_web_config_events_applies_draft_without_closing_session() {
let mut app = create_test_app();