feat(cli): switch profiles from terminal chat - #70657
Conversation
Related: #61205 and #66519 also propose in-session or gateway profile switching. This PR instead performs a clean terminal-only relaunch and explicitly keeps messaging arguments read-only. Maintainer decision needed on the public |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the clean process-boundary approach and the cross-surface coverage. The underlying terminal feature is still absent on current main: cli.py:9600 calls the profile handler without its argument, and the shared status executor at hermes_cli/slash_exec.py:83-106 only reports profile/home.
Problems
- This branch conflicts with current main and predates the registry-owned informational-command refactor (
bcb352eeab) plus the TUI method split (f67ca220ab). Current CLI/profilestatus is owned byhermes_cli/cli_commands_mixin.py:694-705andhermes_cli/slash_exec.py:83-106; a direct application would bypass that parity path. - Issue #55109 asks to preserve the live conversation, whereas this PR deliberately restarts it. That may be the cache-safe choice—
AGENTS.md:1135-1145prohibits changing toolsets or rebuilding prompt state mid-conversation—but it needs an explicit maintainer decision on the public contract.
Suggested changes
- Once the contract is chosen, rework this onto the shared command executor and current split TUI RPC modules; add a temporary-HERMES_HOME E2E assertion that the relaunched process resolves the selected profile.
This is an automated hermes-sweeper review.
| @@ -506,10 +506,29 @@ def isatty(self) -> bool: | |||
| self.new_session() | |||
| _cprint(f"{_DIM}Session reset. New tool configuration is active.{_RST}") | |||
|
|
|||
| def _handle_profile_command(self): | |||
| """Display active profile name and home directory.""" | |||
| def _handle_profile_command(self, command: str = "/profile") -> bool: | |||
There was a problem hiding this comment.
Current main now routes informational /profile through execute_command("profile", ...) (hermes_cli/cli_commands_mixin.py:694-705) and its shared executor (hermes_cli/slash_exec.py:83-106). Please salvage the switch branch into that command architecture so CLI, gateway, and TUI retain the current status-output parity.
There was a problem hiding this comment.
Addressed on the rebased branch.
Status stays on the shared path: bare /profile uses execute_command("profile", …) so CLI / gateway / TUI keep status-output parity. Switching is intentionally outside that executor — sticky write + deferred process relaunch — so we never mutate HERMES_HOME or rebuild tools/prompt mid-conversation.
Resume (new): on /profile <name>, relaunch is now:
--profile <name> --cli|--tui chat [--resume <target-profile last session>]
Lookup is workspace-scoped then global MRU against the target profile’s state.db. Fresh profiles (no sessions) omit --resume and open a clean chat — no hard fail.
Why keep the simple restart (KISS):
- One clear process boundary; no in-process profile surgery
- Preserves prompt-cache / toolset isolation (AGENTS.md mid-conversation constraints)
- Reuses existing
--profile+--resumemachinery instead of a second live-swap path - Easy to reason about and test; fewer failure modes than mid-session bundle swap
Happy to adjust further if maintainers want a different public contract.
| @@ -11319,6 +11319,41 @@ def _(rid, params: dict) -> dict: | |||
| # ── Methods: config ────────────────────────────────────────────────── | |||
|
|
|||
|
|
|||
| @method("profile.switch") | |||
There was a problem hiding this comment.
Current main split @method handlers out of tui_gateway/server.py in f67ca22. Place this RPC in the current registered method module and preserve its registration path rather than restoring a handler to the former monolith.
There was a problem hiding this comment.
Done on rebase: profile.switch lives in tui_gateway/methods_config.py (split RPC module), not the old server.py monolith. Registration goes through the existing methods registry.
Standalone stdio TUI only; busy session and non-stdio transports are rejected. Parent handles exit code 43 and relaunches under the selected profile.
Resume (new): that parent relaunch now uses the shared build_profile_switch_relaunch_argv(..., ui="tui") helper, so TUI also continues the target profile’s last session when one exists (TUI source first, CLI fallback), same KISS process-boundary design as classic CLI.
SummaryTwo PRs address the reported terminal profile-switching gap. PR #6753 adds restart-based interactive Related pull requests
DuplicatesPRs #6753 and #70657 substantially overlap on restart-based terminal Suggested consolidationauthor action: rebase onto main, or split out the part that can merge — retain #70657 as the salvage path for terminal-only switching, port it through the current shared command executor and split TUI RPC modules, and obtain an explicit decision on restart/session-boundary semantics because the issue asks to preserve conversation context. The contributor keep_open review on #70657 is consistent with keeping it open for this rework; #6753 is already closed and can remain closed as the superseded earlier implementation. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I55109(["issue #55109 (open)"])
subgraph Dup6753 ["PRs duplicating each other"]
P6753["PR #6753 (closed)"]
P70657["PR #70657 (open)"]
end
P70657 -->|best fix| I55109
class I55109 open
class P6753 closed
class P70657 open
class P70657 best
class P70657 target
click I55109 "https://github.com/NousResearch/hermes-agent/issues/55109"
click P6753 "https://github.com/NousResearch/hermes-agent/pull/6753"
click P70657 "https://github.com/NousResearch/hermes-agent/pull/70657"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 2 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 34 kB of PR diffs, 10 kB of issue/PR text, 4 kB of discussion (8 comments), 3 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
8e6e37e to
a3a9f60
Compare
On /profile <name>, rebuild the sticky profile switch relaunch argv so the new process continues the target profile's most recent session (workspace- scoped, then global MRU) instead of always opening a blank chat. Fresh profiles without sessions still start cleanly with no --resume flag.
Author update (rebase + resume)Thanks for the reviews and triage (@teknium1, @alt-glitch, @GottZ). Summary of where this PR stands after the latest commits. Review / architecture items (teknium1 sweeper)
New: resume on profile switch (
|
Re: triage on session-boundary / related PRs (@alt-glitch, @GottZ)Agree this PR is intentionally terminal-only clean relaunch, not gateway/in-session live swap (#61205 / #66519 territory). Messaging Contract we are proposing (KISS):
Benefits of keeping it simple:
Full in-process preserve for #55109 remains a maintainer product call; this branch is the salvage path for terminal switching with better post-switch UX via resume. Happy to retitle “Fixes” → “Partial / Related” for #55109 if preferred. |
What does this PR do?
Extends the existing
/profileslash command in terminal chat so/profile <name>cleanly restarts Hermes under the selected profile. Bare/profileremains a read-only status command.The switch is a process boundary rather than an in-place
HERMES_HOMEmutation. This preserves profile isolation and the per-conversation prompt-cache contract: the current CLI/TUI shuts down its agent, tools, memory provider, browser, MCP, and terminal resources before relaunching with--profile <name>.Scope is intentionally terminal-only:
/profile <name>./profileread-only and explain that switching is terminal-only.API_SERVER_PORTand per-profile messaging gateways are unchanged.Related Issue
Fixes #55109
Related: #44063
Type of Change
Changes Made
hermes_cli/cli_commands_mixin.py,cli.py: validate the target profile, set the sticky selection, and defer relaunch until classic CLI cleanup completes.tui_gateway/server.py,ui-tui/src/app/slash/commands/core.ts: add a standalone-TUI-only profile switch RPC and use exit code 43 to request a clean parent relaunch.hermes_cli/main.py: relaunch the Ink TUI under the selected profile after child shutdown.gateway/slash_commands.py: keep messaging profile status read-only and return an explicit terminal-only hint for arguments.website/docs/reference/slash-commands.md: document/profile [name]and Dashboard dropdown behavior.How to Test
hermes profile create coder./profile coder.coderprofile. Run/profileto verify its profile/home./profile coderand confirm it directs you to the profile dropdown without exiting the PTY.Automated checks run:
scripts/run_tests.sh tests/cli/test_profile_command.py tests/hermes_cli/test_tui_resume_flow.py tests/gateway/test_status_command.py -q(96 passed)scripts/run_tests.sh tests/test_tui_gateway_server.py -q -k profile_switch(3 passed)scripts/run_tests.sh tests/gateway/test_session_race_guard.py -q -k profile(1 passed)npm test -- --run src/__tests__/createSlashHandler.test.ts src/__tests__/slashParity.test.ts(80 passed)npm run typecheck && npm run lint && npm run buildpython scripts/check-windows-footguns.py --allgit diff --checkFull
tests/test_tui_gateway_server.pyis currently red on the unrelated existingtest_session_activate_returns_inflight_stream_before_completiontiming assertion (started.wait(2)). It reproduces alone and no changed profile-switch path reaches that test. The other 383 tests in the file pass.Checklist
Code
Documentation & Housekeeping
cli-config.yaml.exampleis N/A: no config keys changedCONTRIBUTING.md/AGENTS.mdis N/A: no architecture or workflow change