Merge remote-tracking branch 'origin/pr/1065' into work/v0.8.34
# Conflicts: # crates/tui/src/commands/mod.rs # crates/tui/src/tui/app.rs # crates/tui/src/tui/ui.rs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Core commands: help, clear, exit, model
|
||||
|
||||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::config::{COMMON_DEEPSEEK_MODELS, normalize_model_name};
|
||||
use crate::localization::{MessageId, tr};
|
||||
@@ -182,6 +183,50 @@ pub fn profile_switch(_app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn workspace_switch(app: &mut App, arg: Option<&str>) -> CommandResult {
|
||||
let Some(raw_path) = arg.map(str::trim).filter(|path| !path.is_empty()) else {
|
||||
return CommandResult::message(format!("Current workspace: {}", app.workspace.display()));
|
||||
};
|
||||
|
||||
let expanded = match expand_workspace_path(raw_path) {
|
||||
Ok(path) => path,
|
||||
Err(message) => return CommandResult::error(message),
|
||||
};
|
||||
let candidate = if expanded.is_absolute() {
|
||||
expanded
|
||||
} else {
|
||||
app.workspace.join(expanded)
|
||||
};
|
||||
|
||||
if !candidate.exists() {
|
||||
return CommandResult::error(format!("Workspace does not exist: {}", candidate.display()));
|
||||
}
|
||||
if !candidate.is_dir() {
|
||||
return CommandResult::error(format!(
|
||||
"Workspace is not a directory: {}",
|
||||
candidate.display()
|
||||
));
|
||||
}
|
||||
|
||||
let workspace = candidate.canonicalize().unwrap_or(candidate);
|
||||
CommandResult::with_message_and_action(
|
||||
format!("Switching workspace to {}...", workspace.display()),
|
||||
AppAction::SwitchWorkspace { workspace },
|
||||
)
|
||||
}
|
||||
|
||||
fn expand_workspace_path(path: &str) -> Result<PathBuf, String> {
|
||||
if path == "~" {
|
||||
return dirs::home_dir().ok_or_else(|| "Could not resolve home directory".to_string());
|
||||
}
|
||||
if let Some(rest) = path.strip_prefix("~/") {
|
||||
let home =
|
||||
dirs::home_dir().ok_or_else(|| "Could not resolve home directory".to_string())?;
|
||||
return Ok(home.join(rest));
|
||||
}
|
||||
Ok(PathBuf::from(path))
|
||||
}
|
||||
|
||||
/// Show `DeepSeek` dashboard and docs links
|
||||
pub fn deepseek_links(app: &mut App) -> CommandResult {
|
||||
let locale = app.ui_locale;
|
||||
@@ -331,6 +376,7 @@ mod tests {
|
||||
use crate::tui::history::HistoryCell;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn create_test_app() -> App {
|
||||
let options = TuiOptions {
|
||||
@@ -506,6 +552,62 @@ mod tests {
|
||||
assert!(matches!(result.action, Some(AppAction::Quit)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_without_arg_shows_current_workspace() {
|
||||
let mut app = create_test_app();
|
||||
let result = workspace_switch(&mut app, None);
|
||||
let msg = result.message.expect("workspace should be shown");
|
||||
assert!(msg.contains("Current workspace:"));
|
||||
assert!(msg.contains("/tmp/test-workspace"));
|
||||
assert!(result.action.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_existing_absolute_dir_returns_switch_action() {
|
||||
let mut app = create_test_app();
|
||||
let dir = tempdir().expect("temp dir");
|
||||
let result = workspace_switch(&mut app, Some(dir.path().to_str().unwrap()));
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(AppAction::SwitchWorkspace { workspace }) if workspace == dir.path().canonicalize().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_relative_dir_resolves_from_current_workspace() {
|
||||
let root = tempdir().expect("temp dir");
|
||||
let child = root.path().join("child");
|
||||
std::fs::create_dir(&child).expect("child dir");
|
||||
let mut app = create_test_app();
|
||||
app.workspace = root.path().to_path_buf();
|
||||
|
||||
let result = workspace_switch(&mut app, Some("child"));
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(AppAction::SwitchWorkspace { workspace }) if workspace == child.canonicalize().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_rejects_missing_path() {
|
||||
let mut app = create_test_app();
|
||||
let result = workspace_switch(&mut app, Some("definitely-missing"));
|
||||
assert!(result.is_error);
|
||||
assert!(result.message.unwrap().contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_rejects_file_path() {
|
||||
let root = tempdir().expect("temp dir");
|
||||
let file = root.path().join("file.txt");
|
||||
std::fs::write(&file, "not a directory").expect("test file");
|
||||
let mut app = create_test_app();
|
||||
|
||||
let result = workspace_switch(&mut app, Some(file.to_str().unwrap()));
|
||||
assert!(result.is_error);
|
||||
assert!(result.message.unwrap().contains("not a directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_change_updates_state() {
|
||||
let mut app = create_test_app();
|
||||
|
||||
@@ -230,6 +230,12 @@ pub const COMMANDS: &[CommandInfo] = &[
|
||||
usage: "/home",
|
||||
description_id: MessageId::CmdHomeDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "workspace",
|
||||
aliases: &["cwd"],
|
||||
usage: "/workspace [path]",
|
||||
description_id: MessageId::CmdWorkspaceDescription,
|
||||
},
|
||||
CommandInfo {
|
||||
name: "note",
|
||||
aliases: &[],
|
||||
@@ -552,6 +558,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult {
|
||||
"links" | "dashboard" | "api" | "lianjie" => core::deepseek_links(app),
|
||||
"feedback" => feedback::feedback(app, arg),
|
||||
"home" | "stats" | "overview" | "zhuye" | "shouye" => core::home_dashboard(app),
|
||||
"workspace" | "cwd" => core::workspace_switch(app, arg),
|
||||
"note" => note::note(app, arg),
|
||||
"memory" => memory::memory(app, arg),
|
||||
"attach" | "image" | "media" | "fujian" => attachment::attach(app, arg),
|
||||
@@ -1057,6 +1064,7 @@ mod tests {
|
||||
use std::ffi::OsString;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::MutexGuard;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn create_test_app() -> App {
|
||||
let options = TuiOptions {
|
||||
@@ -1291,6 +1299,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_workspace_alias_switches_workspace() {
|
||||
let dir = tempdir().expect("temp dir");
|
||||
let mut app = create_test_app();
|
||||
let result = execute(&format!("/cwd {}", dir.path().display()), &mut app);
|
||||
assert!(matches!(
|
||||
result.action,
|
||||
Some(AppAction::SwitchWorkspace { workspace }) if workspace == dir.path().canonicalize().unwrap()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_set_and_deepseek_commands_show_migration_hints() {
|
||||
let mut app = create_test_app();
|
||||
|
||||
@@ -313,6 +313,7 @@ pub enum MessageId {
|
||||
CmdTrustDescription,
|
||||
CmdLspDescription,
|
||||
CmdShareDescription,
|
||||
CmdWorkspaceDescription,
|
||||
CmdUndoDescription,
|
||||
CmdVerboseDescription,
|
||||
CmdCacheAdvice,
|
||||
@@ -543,6 +544,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::CmdTrustDescription,
|
||||
MessageId::CmdLspDescription,
|
||||
MessageId::CmdShareDescription,
|
||||
MessageId::CmdWorkspaceDescription,
|
||||
MessageId::CmdUndoDescription,
|
||||
MessageId::CmdVerboseDescription,
|
||||
MessageId::CmdCacheAdvice,
|
||||
@@ -995,6 +997,7 @@ fn english(id: MessageId) -> &'static str {
|
||||
MessageId::CmdTrustDescription => {
|
||||
"Manage workspace trust and per-path allowlist (`/trust add <path>`, `/trust list`, `/trust on|off`)"
|
||||
}
|
||||
MessageId::CmdWorkspaceDescription => "Show or switch the current workspace",
|
||||
MessageId::CmdUndoDescription => "Remove last message pair",
|
||||
MessageId::CmdVerboseDescription => "Toggle full live thinking in the transcript",
|
||||
MessageId::CmdCacheAdvice => {
|
||||
@@ -1375,6 +1378,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdTrustDescription => {
|
||||
"ワークスペースの信頼設定とパス別許可リストを管理(`/trust add <path>`、`/trust list`、`/trust on|off`)"
|
||||
}
|
||||
MessageId::CmdWorkspaceDescription => "現在のワークスペースを表示または切り替え",
|
||||
MessageId::CmdUndoDescription => "最後のメッセージ対を削除",
|
||||
MessageId::CmdVerboseDescription => "ライブ思考表示の詳細モードを切り替え",
|
||||
MessageId::CmdCacheAdvice => {
|
||||
@@ -1708,6 +1712,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdTrustDescription => {
|
||||
"管理工作区信任与按路径的白名单(`/trust add <path>`、`/trust list`、`/trust on|off`)"
|
||||
}
|
||||
MessageId::CmdWorkspaceDescription => "显示或切换当前工作空间",
|
||||
MessageId::CmdUndoDescription => "移除最后一组消息对",
|
||||
MessageId::CmdVerboseDescription => "切换实时思考内容的完整显示",
|
||||
MessageId::CmdCacheAdvice => {
|
||||
@@ -2049,6 +2054,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdTrustDescription => {
|
||||
"Gerenciar a confiança do workspace e a allowlist por caminho (`/trust add <path>`, `/trust list`, `/trust on|off`)"
|
||||
}
|
||||
MessageId::CmdWorkspaceDescription => "Mostrar ou trocar o workspace atual",
|
||||
MessageId::CmdUndoDescription => "Remover o último par de mensagens",
|
||||
MessageId::CmdVerboseDescription => "Alternar pensamento ao vivo completo no transcript",
|
||||
MessageId::CmdCacheAdvice => {
|
||||
@@ -2434,6 +2440,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
|
||||
MessageId::CmdTrustDescription => {
|
||||
"Gestionar la confianza del workspace y la lista de paths permitidos (`/trust add <ruta>`, `/trust list`, `/trust on|off`)"
|
||||
}
|
||||
MessageId::CmdWorkspaceDescription => "Mostrar o cambiar el workspace actual",
|
||||
MessageId::CmdUndoDescription => "Eliminar el último par de mensajes",
|
||||
MessageId::CmdVerboseDescription => {
|
||||
"Alternar pensamiento en vivo completo en la transcripción"
|
||||
|
||||
@@ -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),
|
||||
@@ -1765,6 +1778,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()));
|
||||
|
||||
@@ -12,7 +12,7 @@ use anyhow::{Context, Result, anyhow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -603,6 +603,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(
|
||||
|
||||
+30
-15
@@ -57,6 +57,31 @@ pub enum OnboardingState {
|
||||
None,
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_skills_dir(
|
||||
workspace: &Path,
|
||||
global_skills_dir: &Path,
|
||||
config: &Config,
|
||||
) -> PathBuf {
|
||||
let agents_skills_dir = workspace.join(".agents").join("skills");
|
||||
if agents_skills_dir.exists() {
|
||||
return agents_skills_dir;
|
||||
}
|
||||
|
||||
let local_skills_dir = workspace.join("skills");
|
||||
if local_skills_dir.exists() {
|
||||
return local_skills_dir;
|
||||
}
|
||||
|
||||
if config.skills_dir.is_none()
|
||||
&& let Some(global_agents) = crate::skills::agents_global_skills_dir()
|
||||
&& global_agents.exists()
|
||||
{
|
||||
return global_agents;
|
||||
}
|
||||
|
||||
global_skills_dir.to_path_buf()
|
||||
}
|
||||
|
||||
fn initial_onboarding_state(
|
||||
skip_onboarding: bool,
|
||||
was_onboarded: bool,
|
||||
@@ -1361,21 +1386,7 @@ impl App {
|
||||
// Initialize plan state
|
||||
let plan_state = new_shared_plan_state();
|
||||
|
||||
let agents_skills_dir = workspace.join(".agents").join("skills");
|
||||
let local_skills_dir = workspace.join("skills");
|
||||
let agents_global_skills_dir = crate::skills::agents_global_skills_dir();
|
||||
let skills_dir = if agents_skills_dir.exists() {
|
||||
agents_skills_dir
|
||||
} else if local_skills_dir.exists() {
|
||||
local_skills_dir
|
||||
} else if config.skills_dir.is_none()
|
||||
&& let Some(global_agents) = agents_global_skills_dir
|
||||
&& global_agents.exists()
|
||||
{
|
||||
global_agents
|
||||
} else {
|
||||
global_skills_dir
|
||||
};
|
||||
let skills_dir = resolve_skills_dir(&workspace, &global_skills_dir, config);
|
||||
let cached_skills = Self::discover_cached_skills(&workspace);
|
||||
|
||||
let input_history = crate::composer_history::load_history();
|
||||
@@ -4062,6 +4073,10 @@ pub enum AppAction {
|
||||
/// Profile name to load.
|
||||
profile: String,
|
||||
},
|
||||
/// Switch the workspace used by tools, hooks, tasks, and session metadata.
|
||||
SwitchWorkspace {
|
||||
workspace: PathBuf,
|
||||
},
|
||||
/// Export and share the current session as a web URL.
|
||||
ShareSession {
|
||||
history_len: usize,
|
||||
|
||||
@@ -44,7 +44,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::llm_client::LlmClient;
|
||||
use crate::models::{
|
||||
ContentBlock, Message, MessageRequest, SystemPrompt, Usage, context_window_for_model,
|
||||
@@ -345,7 +345,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: {}",
|
||||
@@ -5462,6 +5462,9 @@ async fn apply_command_result(
|
||||
AppAction::Mcp(action) => {
|
||||
handle_mcp_ui_action(app, config, action).await;
|
||||
}
|
||||
AppAction::SwitchWorkspace { workspace } => {
|
||||
switch_workspace(app, engine_handle, task_manager, config, workspace).await;
|
||||
}
|
||||
AppAction::SwitchProfile { profile } => {
|
||||
app.config_profile = Some(profile.clone());
|
||||
match Config::load(app.config_path.clone(), Some(&profile)) {
|
||||
@@ -5567,6 +5570,72 @@ fn open_external_url(url: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
if app.is_loading {
|
||||
app.status_message =
|
||||
Some("Cannot switch workspace while a request is running.".to_string());
|
||||
app.add_message(HistoryCell::System {
|
||||
content: "Cannot switch workspace while a request is running.".to_string(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if app.workspace == workspace {
|
||||
app.status_message = Some(format!("Workspace unchanged: {}", workspace.display()));
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
*engine_handle = spawn_engine(engine_config, config);
|
||||
if !app.api_messages.is_empty() {
|
||||
let _ = engine_handle
|
||||
.send(Op::SyncSession {
|
||||
session_id: app.current_session_id.clone(),
|
||||
messages: app.api_messages.clone(),
|
||||
system_prompt: app.system_prompt.clone(),
|
||||
model: app.model.clone(),
|
||||
workspace: workspace.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
app.add_message(HistoryCell::System {
|
||||
content: format!("Switched workspace to {}", workspace.display()),
|
||||
});
|
||||
app.status_message = Some(format!("Workspace: {}", workspace.display()));
|
||||
}
|
||||
|
||||
async fn handle_mcp_ui_action(
|
||||
app: &mut App,
|
||||
config: &Config,
|
||||
@@ -6537,7 +6606,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 {
|
||||
session_id: app.current_session_id.clone(),
|
||||
@@ -6882,7 +6952,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();
|
||||
@@ -6928,7 +6998,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 = session.metadata.cost.session_cost_usd;
|
||||
|
||||
@@ -1198,7 +1198,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());
|
||||
@@ -1249,7 +1249,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);
|
||||
@@ -1269,6 +1269,76 @@ fn apply_loaded_session_resets_unpersisted_telemetry() {
|
||||
assert!(app.session.turn_cache_history.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async 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("old workspace context".to_string());
|
||||
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(".").as_path(),
|
||||
));
|
||||
|
||||
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::execute("/workspace", &mut app);
|
||||
|
||||
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();
|
||||
@@ -3510,7 +3580,7 @@ fn apply_loaded_session_restores_artifact_registry() {
|
||||
storage_path: PathBuf::from("/tmp/tool_outputs/call-big.txt"),
|
||||
});
|
||||
|
||||
let recovered = apply_loaded_session(&mut app, &session);
|
||||
let recovered = apply_loaded_session(&mut app, &Config::default(), &session);
|
||||
|
||||
assert!(!recovered);
|
||||
assert_eq!(app.session_artifacts, session.artifacts);
|
||||
|
||||
Reference in New Issue
Block a user