-
Notifications
You must be signed in to change notification settings - Fork 48.1k
feat(cli): switch profiles from terminal chat #70657
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AnkitArya
wants to merge
2
commits into
NousResearch:main
Choose a base branch
from
AnkitArya:feat/in-chat-profile-switch
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| """Tests for in-chat profile switching in the classic CLI.""" | ||
|
|
||
| from types import SimpleNamespace | ||
| from unittest.mock import patch | ||
|
|
||
| from cli import HermesCLI | ||
|
|
||
|
|
||
| def _call(self_, command): | ||
| return HermesCLI._handle_profile_command(self_, command) | ||
|
|
||
|
|
||
| def test_profile_without_name_shows_runtime_profile(capsys): | ||
| self_ = SimpleNamespace(_pending_relaunch=None) | ||
|
|
||
| with ( | ||
| patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"), | ||
| patch("hermes_constants.display_hermes_home", return_value="~/.hermes/profiles/coder"), | ||
| ): | ||
| result = _call(self_, "/profile") | ||
|
|
||
| assert result is False | ||
| assert self_._pending_relaunch is None | ||
| output = capsys.readouterr().out | ||
| assert "Profile: coder" in output | ||
| assert "Home: ~/.hermes/profiles/coder" in output | ||
|
|
||
|
|
||
| def test_profile_name_sets_sticky_profile_and_requests_clean_relaunch(capsys): | ||
| self_ = SimpleNamespace(_pending_relaunch=None) | ||
|
|
||
| with ( | ||
| patch("hermes_cli.profiles.get_active_profile_name", return_value="default"), | ||
| patch("hermes_cli.profiles.set_active_profile") as set_active, | ||
| patch( | ||
| "hermes_cli.profiles.build_profile_switch_relaunch_argv", | ||
| return_value=["--profile", "coder", "--cli", "chat"], | ||
| ) as build_argv, | ||
| ): | ||
| result = _call(self_, "/profile Coder") | ||
|
|
||
| assert result is True | ||
| set_active.assert_called_once_with("Coder") | ||
| build_argv.assert_called_once_with("coder", ui="cli") | ||
| assert self_._pending_relaunch == ["--profile", "coder", "--cli", "chat"] | ||
| out = capsys.readouterr().out | ||
| assert "Switching to profile 'coder'" in out | ||
| assert "resuming" not in out | ||
|
|
||
|
|
||
| def test_profile_name_relaunches_with_resume_when_target_has_session(capsys): | ||
| self_ = SimpleNamespace(_pending_relaunch=None) | ||
| relaunch = [ | ||
| "--profile", | ||
| "coder", | ||
| "--cli", | ||
| "chat", | ||
| "--resume", | ||
| "20260811_120000_abcdef", | ||
| ] | ||
|
|
||
| with ( | ||
| patch("hermes_cli.profiles.get_active_profile_name", return_value="default"), | ||
| patch("hermes_cli.profiles.set_active_profile"), | ||
| patch( | ||
| "hermes_cli.profiles.build_profile_switch_relaunch_argv", | ||
| return_value=relaunch, | ||
| ), | ||
| ): | ||
| result = _call(self_, "/profile coder") | ||
|
|
||
| assert result is True | ||
| assert self_._pending_relaunch == relaunch | ||
| assert "resuming last session" in capsys.readouterr().out | ||
|
|
||
|
|
||
| def test_profile_name_error_keeps_current_chat(capsys): | ||
| self_ = SimpleNamespace(_pending_relaunch=None) | ||
|
|
||
| with patch( | ||
| "hermes_cli.profiles.set_active_profile", | ||
| side_effect=FileNotFoundError("Profile 'missing' does not exist"), | ||
| ): | ||
| result = _call(self_, "/profile missing") | ||
|
|
||
| assert result is False | ||
| assert self_._pending_relaunch is None | ||
| assert "does not exist" in capsys.readouterr().out | ||
|
|
||
|
|
||
| def test_profile_switch_relaunch_argv_includes_selected_profile(tmp_path, monkeypatch, capsys): | ||
| """E2E-ish: sticky write + relaunch argv resolve the selected profile name.""" | ||
| from hermes_cli.profiles import get_active_profile | ||
|
|
||
| profiles_root = tmp_path / "profiles" | ||
| coder = profiles_root / "coder" | ||
| coder.mkdir(parents=True) | ||
| monkeypatch.setenv("HERMES_HOME", str(tmp_path / "default-home")) | ||
| # Point sticky active_profile file into tmp root via profiles helpers. | ||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles._get_active_profile_path", | ||
| lambda: tmp_path / "active_profile", | ||
| ) | ||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles._get_profiles_root", | ||
| lambda: profiles_root, | ||
| ) | ||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles.profile_exists", | ||
| lambda name: name == "coder", | ||
| ) | ||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles.build_profile_switch_relaunch_argv", | ||
| lambda name, *, ui="cli": ["--profile", name, f"--{ui}", "chat"], | ||
| ) | ||
|
|
||
| self_ = SimpleNamespace(_pending_relaunch=None) | ||
| with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"): | ||
| result = _call(self_, "/profile coder") | ||
|
|
||
| assert result is True | ||
| assert get_active_profile() == "coder" | ||
| assert self_._pending_relaunch == ["--profile", "coder", "--cli", "chat"] | ||
| assert "Switching to profile 'coder'" in capsys.readouterr().out | ||
|
|
||
|
|
||
| def test_build_profile_switch_relaunch_argv_appends_resume_when_session_exists(monkeypatch): | ||
| from hermes_cli.profiles import build_profile_switch_relaunch_argv | ||
|
|
||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles._current_workspace_key", | ||
| lambda: "/tmp/ws", | ||
| ) | ||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles.resolve_profile_last_session", | ||
| lambda name, *, source="cli", workspace_key=None: ( | ||
| "sess-cli" if source == "cli" else None | ||
| ), | ||
| ) | ||
|
|
||
| assert build_profile_switch_relaunch_argv("Coder", ui="cli") == [ | ||
| "--profile", | ||
| "coder", | ||
| "--cli", | ||
| "chat", | ||
| "--resume", | ||
| "sess-cli", | ||
| ] | ||
| assert build_profile_switch_relaunch_argv("coder", ui="tui") == [ | ||
| "--profile", | ||
| "coder", | ||
| "--tui", | ||
| "chat", | ||
| "--resume", | ||
| "sess-cli", | ||
| ] | ||
|
|
||
|
|
||
| def test_build_profile_switch_relaunch_argv_omits_resume_when_no_session(monkeypatch): | ||
| from hermes_cli.profiles import build_profile_switch_relaunch_argv | ||
|
|
||
| monkeypatch.setattr("hermes_cli.profiles._current_workspace_key", lambda: None) | ||
| monkeypatch.setattr( | ||
| "hermes_cli.profiles.resolve_profile_last_session", | ||
| lambda *a, **k: None, | ||
| ) | ||
|
|
||
| assert build_profile_switch_relaunch_argv("coder", ui="cli") == [ | ||
| "--profile", | ||
| "coder", | ||
| "--cli", | ||
| "chat", | ||
| ] | ||
| assert build_profile_switch_relaunch_argv("coder", ui="tui") == [ | ||
| "--profile", | ||
| "coder", | ||
| "--tui", | ||
| "chat", | ||
| ] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Current main now routes informational
/profilethroughexecute_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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed on the rebased branch.
Status stays on the shared path: bare
/profileusesexecute_command("profile", …)so CLI / gateway / TUI keep status-output parity. Switching is intentionally outside that executor — sticky write + deferred process relaunch — so we never mutateHERMES_HOMEor rebuild tools/prompt mid-conversation.Resume (new): on
/profile <name>, relaunch is now:Lookup is workspace-scoped then global MRU against the target profile’s
state.db. Fresh profiles (no sessions) omit--resumeand open a clean chat — no hard fail.Why keep the simple restart (KISS):
--profile+--resumemachinery instead of a second live-swap pathHappy to adjust further if maintainers want a different public contract.