fix(desktop): stabilize project folder sessions (#37586)
* fix(desktop): stabilize project folder sessions Keep desktop folder selection aligned with new sessions and scope TUI gateway cwd through session context so prompts and tools resolve against the selected workspace. * fix(desktop): address review feedback on folder sessions Snapshot sessions before iterating to avoid concurrent-mutation crashes, optional-chain the revealLogs catch, and read console-message args from the correct Electron event/messageDetails positions. * fix(desktop): address second review pass on folder sessions Sync the remembered workspace key with the cwd atom (clear on empty), only load tree children for real directory nodes, and throttle renderer auto-reloads so a deterministic startup crash can't loop forever. * fix(desktop): inherit parent workspace for ephemeral agent tasks Background and preview tasks use ephemeral ids absent from the session map, so pass the parent session cwd into the session context explicitly instead of clearing it back to the gateway launch dir. Also correct the set_session_vars docstring about clear_session_vars semantics. * fix(desktop): validate preview cwd before pinning session context A non-empty but non-existent client cwd would pin an unusable override and silently fall back to the launch dir. Validate once, reuse for both the session context and the terminal override, and fall back to the parent session workspace when invalid. * fix(desktop): harden preview cwd normalization and adopt normalized cwd Guard preview cwd normalization against malformed client paths so a bad input can't fail the whole restart, and adopt the backend's normalized config.get cwd in the no-active-session path so the persisted workspace stays consistent with what the agent uses.
This commit is contained in:
@@ -6,7 +6,12 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
import agent.runtime_cwd as rt
|
||||
from agent.runtime_cwd import resolve_agent_cwd, resolve_context_cwd
|
||||
from agent.runtime_cwd import (
|
||||
clear_session_cwd,
|
||||
resolve_agent_cwd,
|
||||
resolve_context_cwd,
|
||||
set_session_cwd,
|
||||
)
|
||||
|
||||
|
||||
def _raise_oserror(*args, **kwargs):
|
||||
@@ -77,3 +82,48 @@ class TestResolveContextCwd:
|
||||
# than building Path(" ") and resolving garbage under the launch dir.
|
||||
monkeypatch.setenv("TERMINAL_CWD", " ")
|
||||
assert resolve_context_cwd() is None
|
||||
|
||||
|
||||
class TestSessionCwdOverride:
|
||||
"""The #29531 per-session arm: a contextvar cwd wins over TERMINAL_CWD so a
|
||||
multi-session gateway can pin each session to its own folder."""
|
||||
|
||||
def test_session_cwd_overrides_terminal_cwd(self, monkeypatch, tmp_path):
|
||||
other = tmp_path / "other"
|
||||
other.mkdir()
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
||||
token = set_session_cwd(str(other))
|
||||
try:
|
||||
assert resolve_agent_cwd() == other
|
||||
assert resolve_context_cwd() == other
|
||||
finally:
|
||||
rt._SESSION_CWD.reset(token)
|
||||
|
||||
def test_empty_session_cwd_falls_back_to_terminal_cwd(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
||||
token = set_session_cwd("")
|
||||
try:
|
||||
assert resolve_agent_cwd() == tmp_path
|
||||
assert resolve_context_cwd() == tmp_path
|
||||
finally:
|
||||
rt._SESSION_CWD.reset(token)
|
||||
|
||||
def test_clear_session_cwd_restores_terminal_cwd(self, monkeypatch, tmp_path):
|
||||
other = tmp_path / "other"
|
||||
other.mkdir()
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
||||
token = set_session_cwd(str(other))
|
||||
try:
|
||||
clear_session_cwd()
|
||||
assert resolve_agent_cwd() == tmp_path
|
||||
finally:
|
||||
rt._SESSION_CWD.reset(token)
|
||||
|
||||
def test_nonexistent_session_cwd_falls_back(self, monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
|
||||
token = set_session_cwd(str(tmp_path / "gone"))
|
||||
try:
|
||||
# resolve_agent_cwd guards on isdir; a missing session cwd must not win.
|
||||
assert resolve_agent_cwd() == tmp_path
|
||||
finally:
|
||||
rt._SESSION_CWD.reset(token)
|
||||
|
||||
@@ -10,6 +10,55 @@ from unittest.mock import patch
|
||||
from tui_gateway import server
|
||||
|
||||
|
||||
def test_session_context_uses_session_cwd(monkeypatch, tmp_path):
|
||||
"""Desktop/TUI sessions must pin the agent cwd per session.
|
||||
|
||||
The gateway process itself is often launched from apps/desktop in dev, so
|
||||
falling back to os.getcwd() makes agents answer from the desktop app folder
|
||||
even when the sidebar/session cwd is a real project.
|
||||
"""
|
||||
from agent.runtime_cwd import resolve_agent_cwd
|
||||
|
||||
sid = "cwd-sid"
|
||||
session_key = "cwd-key"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
launcher = tmp_path / "apps" / "desktop"
|
||||
launcher.mkdir(parents=True)
|
||||
|
||||
server._sessions[sid] = {"session_key": session_key, "cwd": str(project)}
|
||||
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
||||
monkeypatch.chdir(launcher)
|
||||
|
||||
tokens = server._set_session_context(session_key)
|
||||
try:
|
||||
assert resolve_agent_cwd() == project
|
||||
finally:
|
||||
server._clear_session_context(tokens)
|
||||
server._sessions.pop(sid, None)
|
||||
|
||||
|
||||
def test_session_context_explicit_cwd_for_ephemeral_task(monkeypatch, tmp_path):
|
||||
"""Background/preview tasks use ephemeral ids absent from `_sessions`, so the
|
||||
parent workspace is passed explicitly; it must pin instead of clearing back
|
||||
to the gateway launch dir."""
|
||||
from agent.runtime_cwd import resolve_agent_cwd
|
||||
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
launcher = tmp_path / "apps" / "desktop"
|
||||
launcher.mkdir(parents=True)
|
||||
|
||||
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
||||
monkeypatch.chdir(launcher)
|
||||
|
||||
tokens = server._set_session_context("bg_deadbe", cwd=str(project))
|
||||
try:
|
||||
assert resolve_agent_cwd() == project
|
||||
finally:
|
||||
server._clear_session_context(tokens)
|
||||
|
||||
|
||||
class _ChunkyStdout:
|
||||
def __init__(self):
|
||||
self.parts: list[str] = []
|
||||
|
||||
Reference in New Issue
Block a user