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:
brooklyn!
2026-06-02 20:23:09 +00:00
committed by GitHub
parent 79bfddd37c
commit 31c40c72c0
14 changed files with 493 additions and 51 deletions
+51 -1
View File
@@ -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)
+49
View File
@@ -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] = []