Inspired by Amp: pick the workspace a dashboard chat starts in
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.
This commit is contained in:
@@ -297,7 +297,8 @@ def _ws_auth_ok(ws: "WebSocket") -> bool:
|
||||
|
||||
def _resolve_chat_argv(
|
||||
resume: Optional[str] = None, sidecar_url: Optional[str] = None, profile: Optional[str] = None,
|
||||
active_session_file: Optional[str] = None) -> tuple[list[str], Optional[str], Optional[dict]]:
|
||||
active_session_file: Optional[str] = None,
|
||||
workspace_cwd: Optional[str] = None) -> tuple[list[str], Optional[str], Optional[dict]]:
|
||||
"""Resolve the argv + cwd + env for the chat PTY (what ``hermes --tui`` runs).
|
||||
|
||||
Tests monkeypatch this with a tiny fake command. Env contract: resume goes
|
||||
@@ -306,7 +307,12 @@ def _resolve_chat_argv(
|
||||
in-memory gateway but is SKIPPED for profile-scoped chats (that gateway runs
|
||||
under the dashboard's own profile, so a scoped chat spawns its own);
|
||||
``profile`` scopes the ENTIRE chat by pointing ``HERMES_HOME`` at the profile
|
||||
dir, the same propagation ``hermes -p <name>`` performs.
|
||||
dir, the same propagation ``hermes -p <name>`` performs. ``workspace_cwd``
|
||||
(an already-validated host directory, ``chat_workspaces.resolve_chat_cwd``)
|
||||
is the workspace the user picked for a FRESH chat: it becomes ``HERMES_CWD``
|
||||
(where a self-spawned gateway starts) and ``HERMES_TUI_CWD`` (what the TUI
|
||||
passes as the explicit ``cwd`` of ``session.create`` when attached to the
|
||||
in-memory gateway, whose own cwd is the dashboard's launch dir).
|
||||
"""
|
||||
from hermes_cli.web_server_profiles import _config_profile_scope, _resolve_profile_dir
|
||||
from hermes_cli.web_server_sessions import _open_session_db_for_profile, _session_latest_descendant
|
||||
@@ -341,6 +347,9 @@ def _resolve_chat_argv(
|
||||
apply_terminal_config_to_env(env=env)
|
||||
except Exception:
|
||||
_log.warning("Failed to apply terminal config bridge for dashboard chat", exc_info=True)
|
||||
if workspace_cwd:
|
||||
env["HERMES_CWD"] = workspace_cwd
|
||||
env["HERMES_TUI_CWD"] = workspace_cwd
|
||||
_apply_tui_python_env(env)
|
||||
env.setdefault("NODE_ENV", "production")
|
||||
# Mouse tracking would swallow wheel events the browser needs for
|
||||
@@ -430,13 +439,16 @@ def _build_sidecar_url(channel: str) -> Optional[str]:
|
||||
|
||||
async def _resolve_chat_argv_async(
|
||||
resume: Optional[str] = None, sidecar_url: Optional[str] = None, profile: Optional[str] = None,
|
||||
active_session_file: Optional[str] = None) -> tuple[list[str], Optional[str], Optional[dict]]:
|
||||
active_session_file: Optional[str] = None,
|
||||
workspace_cwd: Optional[str] = None) -> tuple[list[str], Optional[str], Optional[dict]]:
|
||||
"""Resolve chat argv off the event loop (it may run ``npm run build``); the
|
||||
async lock keeps one-build-at-a-time without parking worker threads."""
|
||||
from hermes_cli.web_server import _get_chat_argv_lock, app
|
||||
kwargs = {"resume": resume, "sidecar_url": sidecar_url, "profile": profile}
|
||||
if active_session_file is not None:
|
||||
kwargs["active_session_file"] = active_session_file
|
||||
if workspace_cwd is not None:
|
||||
kwargs["workspace_cwd"] = workspace_cwd
|
||||
|
||||
async with _get_chat_argv_lock(app):
|
||||
return await asyncio.to_thread(_resolve_chat_argv, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user