fix(tui): sync runtime state on workspace switch
This commit is contained in:
@@ -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()));
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user