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 15:23:09 -05:00
committed by GitHub
parent 79bfddd37c
commit 31c40c72c0
14 changed files with 493 additions and 51 deletions

View File

@ -809,11 +809,31 @@ def _save_cfg(cfg: dict):
_cfg_mtime = None
def _set_session_context(session_key: str) -> list:
def _cwd_for_session_key(session_key: str) -> str:
"""Reverse-map session_key to the session's logical cwd.
Snapshots ``_sessions`` first: concurrent RPC handlers mutate it from the
thread pool, so iterating the live view risks ``RuntimeError: dictionary
changed size during iteration``.
"""
if not session_key:
return ""
for sess in list(_sessions.values()):
if sess.get("session_key") == session_key:
return str(sess.get("cwd") or "")
return ""
def _set_session_context(session_key: str, cwd: str | None = None) -> list:
try:
from gateway.session_context import set_session_vars
return set_session_vars(session_key=session_key)
# Ephemeral task IDs (background, preview) aren't in `_sessions`, so the
# reverse-map returns "" and would clear the cwd override. Callers that
# know the parent workspace pass it explicitly so spawned agents inherit
# it instead of falling back to the gateway launch dir.
resolved = cwd if cwd is not None else _cwd_for_session_key(session_key)
return set_session_vars(session_key=session_key, cwd=resolved)
except Exception:
return []
@ -2764,6 +2784,7 @@ def _(rid, params: dict) -> dict:
explicit_cwd = bool(raw_cwd) and os.path.isdir(os.path.abspath(os.path.expanduser(raw_cwd)))
except Exception:
explicit_cwd = False
resolved_cwd = _completion_cwd(params)
_enable_gateway_prompts()
ready = threading.Event()
@ -2782,7 +2803,7 @@ def _(rid, params: dict) -> dict:
"history_lock": threading.Lock(),
"history_version": 0,
"image_counter": 0,
"cwd": _completion_cwd(params),
"cwd": resolved_cwd,
"inflight_turn": None,
"last_active": now,
"pending_title": title or None,
@ -4624,7 +4645,7 @@ def _(rid, params: dict) -> dict:
task_id = f"bg_{uuid.uuid4().hex[:6]}"
def run():
session_tokens = _set_session_context(task_id)
session_tokens = _set_session_context(task_id, cwd=_session_cwd(session))
try:
from run_agent import AIAgent
@ -4709,14 +4730,25 @@ def _(rid, params: dict) -> dict:
if line
)
# Normalize defensively: a malformed client path (embedded NUL, etc.) must
# not blow up the whole restart — treat it as "no validated cwd".
try:
preview_cwd = os.path.abspath(os.path.expanduser(cwd)) if cwd else ""
if preview_cwd and not os.path.isdir(preview_cwd):
preview_cwd = ""
except Exception:
preview_cwd = ""
def run():
session_tokens = _set_session_context(task_id)
# Pin the validated preview cwd, else the parent workspace — never an
# invalid client path, which would silently fall back to the launch dir.
session_tokens = _set_session_context(task_id, cwd=(preview_cwd or _session_cwd(session)))
try:
from run_agent import AIAgent
from tools.terminal_tool import register_task_env_overrides
if cwd and os.path.isdir(os.path.abspath(os.path.expanduser(cwd))):
register_task_env_overrides(task_id, {"cwd": os.path.abspath(os.path.expanduser(cwd))})
if preview_cwd:
register_task_env_overrides(task_id, {"cwd": preview_cwd})
history_note = (
f" (with {len(parent_history)} parent-session messages of context)"