diff --git a/crates/tui/src/mcp.rs b/crates/tui/src/mcp.rs index 39d5875e..dea45276 100644 --- a/crates/tui/src/mcp.rs +++ b/crates/tui/src/mcp.rs @@ -1129,6 +1129,7 @@ fn is_connection_closed_error_text(err: &str) -> bool { || err.contains("connection reset") || err.contains("broken pipe") || err.contains("unexpected eof") + || err.contains("forcibly closed") } fn parse_sse_message_data(body: &str) -> Vec> { @@ -4401,6 +4402,14 @@ mod tests { is_mcp_stale_session_error(&err), "reset legacy SSE POST should force reconnect before retry" ); + + let err = anyhow::anyhow!( + "MCP SSE POST send failed (transport=sse endpoint=http://127.0.0.1:123/messages): An existing connection was forcibly closed by the remote host." + ); + assert!( + is_mcp_stale_session_error(&err), + "Windows reset wording should force reconnect before retry" + ); } #[tokio::test] diff --git a/crates/tui/src/project_context.rs b/crates/tui/src/project_context.rs index f8dfa6d6..54c8300f 100644 --- a/crates/tui/src/project_context.rs +++ b/crates/tui/src/project_context.rs @@ -683,17 +683,18 @@ fn load_project_context_with_parents_and_home( workspace: &Path, home_dir: Option<&Path>, ) -> ProjectContext { + let workspace_canonical = canonicalize_workspace_or_keep(workspace); let mut ctx = load_project_context(workspace); let parent_search_stop = project_context_parent_search_stop_dir(); // If no context found in workspace, check parent directories if !ctx.has_instructions() { - let mut current = workspace.parent(); + let mut current = workspace_canonical.parent(); while let Some(parent) = current { if parent_search_stop .as_deref() - .is_some_and(|stop| paths_equal_after_canonicalizing(parent, stop)) + .is_some_and(|stop| parent == stop) { break; } @@ -782,7 +783,7 @@ pub(crate) fn project_context_cache_candidate_paths( while let Some(dir) = current { if parent_search_stop .as_deref() - .is_some_and(|stop| paths_equal_after_canonicalizing(dir, stop)) + .is_some_and(|stop| dir == stop) { break; } @@ -856,10 +857,6 @@ fn project_context_parent_search_stop_dir() -> Option { dirs::home_dir().map(|home| canonicalize_workspace_or_keep(&home)) } -fn paths_equal_after_canonicalizing(left: &Path, right: &Path) -> bool { - canonicalize_workspace_or_keep(left) == canonicalize_workspace_or_keep(right) -} - /// Combine global user-wide preferences with a project-local /// AGENTS.md/CLAUDE.md/instructions.md. Global comes first so /// workspace-specific rules can override it — the model reads in declared diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index d4cb68d5..1e8136b9 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -2710,12 +2710,12 @@ mod tests { // surface (engine.rs builds the system prompt via this fn or // its sibling _and_skills variant on every turn). let _env_guard = crate::test_support::lock_test_env(); - let tmp = tempdir().expect("tempdir"); - let home = tmp.path().join("home"); - let _home = EnvVarGuard::set("HOME", home.as_os_str()); - let _userprofile = EnvVarGuard::set("USERPROFILE", home.as_os_str()); + let workspace_tmp = tempdir().expect("workspace tempdir"); + let home_tmp = tempdir().expect("home tempdir"); + let _home = EnvVarGuard::set("HOME", home_tmp.path().as_os_str()); + let _userprofile = EnvVarGuard::set("USERPROFILE", home_tmp.path().as_os_str()); let _skills_dir = EnvVarGuard::remove("DEEPSEEK_SKILLS_DIR"); - let workspace = tmp.path(); + let workspace = workspace_tmp.path(); for mode in [AppMode::Agent, AppMode::Yolo, AppMode::Plan] { let a = match system_prompt_for_mode_with_context(mode, workspace, None) {