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:
@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user