Skip to content

Commit da1d902

Browse files
committed
Use chat metadata cwd for session working directory
Read cwd from YChat metadata (set by the frontend when a chat is opened) and resolve it against root_dir. Falls back to root_dir then get_chat_dir() when chat metadata has no cwd. Session creation is deferred from __init__ to the first process_message call so the Y.js metadata has time to sync from the browser.
1 parent 3198f53 commit da1d902

3 files changed

Lines changed: 63 additions & 13 deletions

File tree

jupyter_ai_acp_client/base_acp_persona.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class BaseAcpPersona(BasePersona):
4242
Developers should always use `self.get_client()`.
4343
"""
4444

45-
_client_session_future: Task[NewSessionResponse | LoadSessionResponse]
45+
_client_session_future: Task[NewSessionResponse | LoadSessionResponse] | None
4646
"""
4747
The future that yields the ACP client session info. Each instance of an ACP
4848
persona has a unique session ID, i.e. each chat reserves a unique session.
@@ -93,9 +93,7 @@ def __init__(self, *args, executable: list[str], **kwargs):
9393
self._init_client()
9494
)
9595

96-
self._client_session_future = self.event_loop.create_task(
97-
self._init_client_session()
98-
)
96+
self._client_session_future = None
9997
self._acp_slash_commands = []
10098

10199
async def before_agent_subprocess(self) -> None:
@@ -216,17 +214,24 @@ async def get_client(self) -> JaiAcpClient:
216214
"""
217215
return await self.__class__._client_future
218216

217+
async def _ensure_client_session(self):
218+
if self._client_session_future is None:
219+
self._client_session_future = self.event_loop.create_task(
220+
self._init_client_session()
221+
)
222+
return await self._client_session_future
223+
219224
async def get_session_response(self) -> NewSessionResponse | LoadSessionResponse:
220225
"""
221226
Safely returns the ACP session response for this chat.
222227
"""
223-
return await self._client_session_future
228+
return await self._ensure_client_session()
224229

225230
async def get_session_id(self) -> str:
226231
"""
227232
Safely returns the ACP session ID assigned to this chat.
228233
"""
229-
await self._client_session_future
234+
await self._ensure_client_session()
230235
# session ID should always be stored in chat metadata after client
231236
# session was created or loaded.
232237
session_ids = self._get_existing_sessions()

jupyter_ai_acp_client/default_acp_client.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
import logging
3+
import os
34
from pathlib import Path
45
from typing import Any, Awaitable
56
from time import time
@@ -163,6 +164,13 @@ async def _get_mcp_servers(self, persona: BasePersona) -> list[AcpMcpServerStdio
163164

164165
return mcp_servers
165166

167+
def _resolve_cwd(self, persona: BasePersona) -> str:
168+
root_dir = persona.parent.root_dir
169+
chat_cwd = persona.ychat.get_metadata().get("cwd")
170+
if root_dir and chat_cwd and chat_cwd != "/":
171+
return os.path.join(root_dir, chat_cwd)
172+
return root_dir or persona.get_chat_dir()
173+
166174
async def create_session(self, persona: BasePersona) -> NewSessionResponse:
167175
"""
168176
Create an ACP agent session through this client scoped to a
@@ -171,7 +179,7 @@ async def create_session(self, persona: BasePersona) -> NewSessionResponse:
171179
"""
172180
conn = await self.get_connection()
173181
mcp_servers = await self._get_mcp_servers(persona)
174-
cwd = persona.parent.root_dir or persona.get_chat_dir()
182+
cwd = self._resolve_cwd(persona)
175183
session = await conn.new_session(cwd=cwd, mcp_servers=mcp_servers)
176184
self._personas_by_session[session.session_id] = persona
177185
return session
@@ -212,7 +220,7 @@ async def _load_session_rpc(self, persona: BasePersona, session_id: str) -> Load
212220
"""
213221
conn = await self.get_connection()
214222
mcp_servers = await self._get_mcp_servers(persona)
215-
cwd = persona.parent.root_dir or persona.get_chat_dir()
223+
cwd = self._resolve_cwd(persona)
216224
response = await conn.load_session(cwd=cwd, session_id=session_id, mcp_servers=mcp_servers)
217225
self._personas_by_session[session_id] = persona
218226
return response

jupyter_ai_acp_client/tests/test_default_acp_client.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,48 @@ async def _failing_rpc(*args, **kwargs):
244244
class TestSessionCwd:
245245
"""Tests for session cwd resolution in create_session and _load_session_rpc."""
246246

247-
async def test_create_session_uses_root_dir(self):
248-
"""create_session uses persona.parent.root_dir as cwd."""
247+
async def test_create_session_uses_chat_metadata_cwd(self):
248+
"""create_session joins chat metadata cwd with root_dir."""
249249
client, conn, _ = _make_client_and_persona()
250250
client._get_mcp_servers = AsyncMock(return_value=[])
251251

252252
persona = MagicMock()
253253
persona.parent.root_dir = "/home/user/notebooks"
254+
persona.ychat.get_metadata.return_value = {"cwd": "projects/my-repo"}
255+
persona.get_chat_dir.return_value = "/home/user/notebooks/.jupyter/chats"
256+
257+
conn.new_session = AsyncMock(return_value=MagicMock(session_id="s1"))
258+
259+
await client.create_session(persona)
260+
261+
conn.new_session.assert_called_once()
262+
assert conn.new_session.call_args.kwargs["cwd"] == "/home/user/notebooks/projects/my-repo"
263+
264+
async def test_create_session_uses_root_dir_when_no_chat_cwd(self):
265+
"""create_session falls back to root_dir when chat metadata has no cwd."""
266+
client, conn, _ = _make_client_and_persona()
267+
client._get_mcp_servers = AsyncMock(return_value=[])
268+
269+
persona = MagicMock()
270+
persona.parent.root_dir = "/home/user/notebooks"
271+
persona.ychat.get_metadata.return_value = {}
272+
persona.get_chat_dir.return_value = "/home/user/notebooks/.jupyter/chats"
273+
274+
conn.new_session = AsyncMock(return_value=MagicMock(session_id="s1"))
275+
276+
await client.create_session(persona)
277+
278+
conn.new_session.assert_called_once()
279+
assert conn.new_session.call_args.kwargs["cwd"] == "/home/user/notebooks"
280+
281+
async def test_create_session_uses_root_dir_when_chat_cwd_is_root(self):
282+
"""create_session uses root_dir when chat cwd is '/' (file browser root)."""
283+
client, conn, _ = _make_client_and_persona()
284+
client._get_mcp_servers = AsyncMock(return_value=[])
285+
286+
persona = MagicMock()
287+
persona.parent.root_dir = "/home/user/notebooks"
288+
persona.ychat.get_metadata.return_value = {"cwd": "/"}
254289
persona.get_chat_dir.return_value = "/home/user/notebooks/.jupyter/chats"
255290

256291
conn.new_session = AsyncMock(return_value=MagicMock(session_id="s1"))
@@ -267,6 +302,7 @@ async def test_create_session_falls_back_to_chat_dir(self):
267302

268303
persona = MagicMock()
269304
persona.parent.root_dir = None
305+
persona.ychat.get_metadata.return_value = {}
270306
persona.get_chat_dir.return_value = "/home/user/.jupyter/chats"
271307

272308
conn.new_session = AsyncMock(return_value=MagicMock(session_id="s1"))
@@ -276,18 +312,19 @@ async def test_create_session_falls_back_to_chat_dir(self):
276312
conn.new_session.assert_called_once()
277313
assert conn.new_session.call_args.kwargs["cwd"] == "/home/user/.jupyter/chats"
278314

279-
async def test_load_session_uses_root_dir(self):
280-
"""_load_session_rpc uses persona.parent.root_dir as cwd."""
315+
async def test_load_session_uses_chat_metadata_cwd(self):
316+
"""_load_session_rpc joins chat metadata cwd with root_dir."""
281317
client, conn, _ = _make_client_and_persona()
282318
client._get_mcp_servers = AsyncMock(return_value=[])
283319

284320
persona = MagicMock()
285321
persona.parent.root_dir = "/home/user/notebooks"
322+
persona.ychat.get_metadata.return_value = {"cwd": "projects/my-repo"}
286323
persona.get_chat_dir.return_value = "/home/user/notebooks/.jupyter/chats"
287324

288325
conn.load_session = AsyncMock(return_value=MagicMock(session_id="s1"))
289326

290327
await client._load_session_rpc(persona, "s1")
291328

292329
conn.load_session.assert_called_once()
293-
assert conn.load_session.call_args.kwargs["cwd"] == "/home/user/notebooks"
330+
assert conn.load_session.call_args.kwargs["cwd"] == "/home/user/notebooks/projects/my-repo"

0 commit comments

Comments
 (0)