A fresh dashboard /chat always spawned the TUI in the dashboard process's launch directory, so from a phone or any browser there was no way to aim a new session at a specific repository. Amp's runners now serve many directories and the web composer offers a picker of the runner's projects and discovered git checkouts; this ports that mechanism onto the surface Hermes already has: the dashboard is the phone/web front, the host's projects.db + repo-discovery cache are the served directories. - GET /api/chat/workspaces: the profile's projects (with folders) and discovered repos (session-derived + scanned), default_cwd, home; ?scan=1 rescans desktop.repo_scan_roots on the host, so headless installs (no Desktop to populate the cache) discover repos too. - /api/pty?cwd=<dir>: validated (existing directory, fail-closed 400 via the PTY error path) and forwarded to the TUI child as HERMES_CWD (self-spawned gateway cwd) + HERMES_TUI_CWD (explicit cwd on session.create for the dashboard's in-memory gateway, whose own cwd is the launch dir). Resumed sessions ignore it. - ui-tui: session.create carries cwd when HERMES_TUI_CWD is set, so /new inside a dashboard chat stays in the picked workspace too. - web: workspace selector in the Chat rail above "New chat" (projects, repos by recency, Other path…, rescan), remembered per profile in localStorage; 17 locales. - docs: web-dashboard.md rail + REST sections. Live E2E: real `hermes dashboard` under a scratch HOME with two git repos under desktop.repo_scan_roots -> /api/chat/workspaces?scan=1 lists both; /api/pty?cwd=repo-a -> TUI status bar shows ~/code/repo-a; /api/pty?cwd=<missing> -> "Working directory does not exist" + close.
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
"""Dashboard chat workspace picker: ``GET /api/chat/workspaces`` + ``/api/pty?cwd=``.
|
|
|
|
A fresh dashboard chat can be aimed at a host directory (the same projects/repos the
|
|
Desktop sidebar lists); the pick reaches the TUI as ``HERMES_CWD``/``HERMES_TUI_CWD`` and a
|
|
dead path fails closed instead of silently landing in the dashboard's launch dir.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from hermes_cli import web_server, web_server_chat
|
|
from hermes_cli.web_routers import chat_workspaces
|
|
|
|
pytest.importorskip("starlette.testclient")
|
|
from starlette.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
previous = getattr(web_server.app.state, "auth_required", None)
|
|
web_server.app.state.auth_required = False
|
|
test_client = TestClient(web_server.app)
|
|
test_client.headers[web_server._SESSION_HEADER_NAME] = web_server._SESSION_TOKEN
|
|
try:
|
|
yield test_client
|
|
finally:
|
|
if previous is None:
|
|
try:
|
|
delattr(web_server.app.state, "auth_required")
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
web_server.app.state.auth_required = previous
|
|
|
|
|
|
def test_workspaces_lists_projects_and_discovered_repos(client, tmp_path):
|
|
"""Explicit project folders AND the discovery cache both surface; the payload names the
|
|
default cwd a chat lands in when nothing is picked."""
|
|
from hermes_cli import projects_db as pdb
|
|
|
|
proj_dir = tmp_path / "proj"
|
|
proj_dir.mkdir()
|
|
repo_dir = tmp_path / "scanned"
|
|
repo_dir.mkdir()
|
|
with pdb.connect_closing() as conn:
|
|
pdb.create_project(conn, name="Demo", folders=[str(proj_dir)])
|
|
pdb.record_discovered_repos(conn, [(str(repo_dir), "scanned")], replace=True, policy_key="k")
|
|
|
|
body = client.get("/api/chat/workspaces").json()
|
|
|
|
assert [p["name"] for p in body["projects"]] == ["Demo"]
|
|
assert body["projects"][0]["folders"][0]["path"] == str(proj_dir)
|
|
assert str(repo_dir) in {r["root"] for r in body["repos"]}
|
|
assert Path(body["default_cwd"]).is_dir()
|
|
assert body["home"] == str(Path.home())
|
|
|
|
|
|
def test_resolve_chat_cwd_fails_closed_and_reaches_the_tui_env(monkeypatch, tmp_path):
|
|
"""A picked directory becomes HERMES_CWD + HERMES_TUI_CWD on the PTY child; a missing one is a
|
|
400 (never the launch-dir fallback); unset means no override."""
|
|
import hermes_cli.main_tui_launch as tui_launch
|
|
|
|
monkeypatch.setattr(
|
|
tui_launch, "_make_tui_argv", lambda *_a, **_k: (["node", "fake-tui.js"], tmp_path))
|
|
|
|
assert chat_workspaces.resolve_chat_cwd("") is None
|
|
with pytest.raises(HTTPException) as exc:
|
|
chat_workspaces.resolve_chat_cwd(str(tmp_path / "gone"))
|
|
assert exc.value.status_code == 400
|
|
|
|
picked = chat_workspaces.resolve_chat_cwd(str(tmp_path))
|
|
_argv, _cwd, env = web_server_chat._resolve_chat_argv(workspace_cwd=picked)
|
|
assert env["HERMES_CWD"] == str(tmp_path)
|
|
assert env["HERMES_TUI_CWD"] == str(tmp_path)
|
|
|
|
_argv, _cwd, env = web_server_chat._resolve_chat_argv()
|
|
assert "HERMES_TUI_CWD" not in env
|