Skip to content

feat(cli): switch profiles from terminal chat - #70657

Open
AnkitArya wants to merge 2 commits into
NousResearch:mainfrom
AnkitArya:feat/in-chat-profile-switch
Open

feat(cli): switch profiles from terminal chat#70657
AnkitArya wants to merge 2 commits into
NousResearch:mainfrom
AnkitArya:feat/in-chat-profile-switch

Conversation

@AnkitArya

Copy link
Copy Markdown

What does this PR do?

Extends the existing /profile slash command in terminal chat so /profile <name> cleanly restarts Hermes under the selected profile. Bare /profile remains a read-only status command.

The switch is a process boundary rather than an in-place HERMES_HOME mutation. 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:

  • Classic prompt-toolkit CLI and standalone Ink TUI support /profile <name>.
  • Dashboard Chat directs users to its existing profile dropdown.
  • Desktop keeps its existing per-profile backend pool.
  • Messaging gateways keep /profile read-only and explain that switching is terminal-only.
  • API_SERVER_PORT and per-profile messaging gateways are unchanged.

Related Issue

Fixes #55109
Related: #44063

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

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.
  • Tests cover classic CLI switching, missing profiles, busy sessions, WebSocket rejection, dashboard routing, and parent relaunch.
  • website/docs/reference/slash-commands.md: document /profile [name] and Dashboard dropdown behavior.

How to Test

  1. Create or use an existing named profile: hermes profile create coder.
  2. Start classic CLI or standalone TUI and run /profile coder.
  3. Confirm the current process exits cleanly and a fresh chat starts with the coder profile. Run /profile to verify its profile/home.
  4. In Dashboard Chat, run /profile coder and 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 build
  • Ruff on changed Python files
  • python scripts/check-windows-footguns.py --all
  • Prettier check and git diff --check

Full tests/test_tui_gateway_server.py is currently red on the unrelated existing test_session_activate_returns_inflight_stream_before_completion timing 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

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched existing issues and PRs before implementation
  • My PR contains only changes related to this feature
  • I've run the full suite and all tests pass (one unrelated timing test remains red; detailed above)
  • I've added tests for the change
  • I've tested on macOS

Documentation & Housekeeping

  • Relevant slash-command documentation is updated
  • cli-config.yaml.example is N/A: no config keys changed
  • CONTRIBUTING.md / AGENTS.md is N/A: no architecture or workflow change
  • Cross-platform impact was checked, including the Windows footgun scan
  • Tool schemas are N/A: no model tools changed

@alt-glitch alt-glitch added type/feature New feature or request comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) area/profiles Multi-profile isolation, HERMES_HOME scoping area/sessions Session lifecycle, resume, persistence, history P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation labels Jul 24, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

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 /profile <name> contract and session-boundary semantics.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /profile status is owned by hermes_cli/cli_commands_mixin.py:694-705 and hermes_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-1145 prohibits 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:

Copy link
Copy Markdown
Contributor

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 /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.

Copy link
Copy Markdown
Author

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 /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 + --resume machinery 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.

Comment thread tui_gateway/server.py Outdated
@@ -11319,6 +11319,41 @@ def _(rid, params: dict) -> dict:
# ── Methods: config ──────────────────────────────────────────────────


@method("profile.switch")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary

Two PRs address the reported terminal profile-switching gap. PR #6753 adds restart-based interactive /profile listing and switching with focused CLI tests and documentation, while PR #70657 extends restart-based switching across the classic CLI and standalone TUI, preserves read-only gateway behavior, and adds cross-surface tests; both use a process boundary rather than preserving the live conversation in place.

Related pull requests

  • feat(cli): add restart-based profile switching to /profile #6753 [closed] duplicate — (+217/-19) — n/a: adds restart-based interactive CLI /profile status, listing, and switching, with focused tests and documentation, directly addressing terminal profile switching but not live in-process conversation preservation. Closed as shelved cleanup; its contributor states the branch is stale against current profile/home semantics and should be rebuilt fresh if the feature is retained.
  • feat(cli): switch profiles from terminal chat #70657 best fix — (+352/-13) — n/a: adds terminal-only profile switching through a clean relaunch for the classic CLI and standalone TUI, keeps gateway /profile read-only, and adds RPC, parity, and regression coverage; the diff does not preserve the live conversation. The contributor keep_open review records medium salvageability and requires rework onto the current command-executor and split-TUI architecture, explicitly addressing the issue's session-boundary contract and the cited current-main changes (bcb352eeab, f67ca220ab, hermes_cli/cli_commands_mixin.py:694-705, hermes_cli/slash_exec.py:83-106).

Duplicates

PRs #6753 and #70657 substantially overlap on restart-based terminal /profile <name> switching. PR #6753 is the narrower earlier CLI implementation; PR #70657 supersedes that approach in scope but currently needs rework against current main.

Suggested consolidation

author 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 graph

flowchart 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"
Loading

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.

@AnkitArya
AnkitArya force-pushed the feat/in-chat-profile-switch branch from 8e6e37e to a3a9f60 Compare August 11, 2026 11:20
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.
@AnkitArya

Copy link
Copy Markdown
Author

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)

Ask Status
Rebase onto current main (executor + TUI split) Done
Status via shared execute_command("profile") Done — bare /profile only
profile.switch in split TUI RPC module Done — tui_gateway/methods_config.py
Sticky + relaunch resolves selected profile Done — tests cover CLI + exit 43
Live in-process preserve (#55109 full ask) Not in this PR — needs maintainer contract call

New: resume on profile switch (e2c82d6)

/profile <name> still does a clean process relaunch (sticky profile + parent relaunch). After relaunch it now continues the target profile’s last session when one exists:

hermes --profile <name> --cli|--tui chat [--resume <id>]
  • Lookup: workspace-scoped MRU, then global MRU, against the target profile’s state.db (read-only)
  • TUI tries source=tui then falls back to cli
  • No prior session → normal blank chat (no hard fail on --continue)
  • Does not transplant the current live transcript into another profile (sessions stay isolated)

Why we keep the simple restart (KISS)

We deliberately prefer process-boundary switch + optional resume over mid-session bundle swap:

  1. One mechanism — reuse existing --profile and --resume; no parallel “live profile mutate” path
  2. Cache- and toolset-safe — avoids mid-conversation toolset/prompt rebuild (AGENTS.md constraints called out in review)
  3. Clear isolation — each profile keeps its own HERMES_HOME / state.db; no half-swapped agent state
  4. Small surface — easier to test, reason about, and maintain than in-process model+toolset+persona surgery
  5. Good enough UX for mode flipping — designer ↔ coder becomes “switch profile → land on that profile’s last chat”, not a blank slate every time

Contract / #55109 (still needs maintainer decision)

  • This PR’s public contract: terminal-only /profile <name> = clean restart under that profile, plus resume of that profile’s last session when available. Gateway stays read-only; Dashboard uses its dropdown.
  • Full Feature: switch the whole profile (model + toolset + persona) mid-session with /profile <name> #55109 ask (preserve the same live conversation while swapping the whole bundle in-process) is a different, larger design. Happy to split that or mark this PR as a partial/related fix once maintainers choose.

Test focus

  • tests/cli/test_profile_command.py — switch, resume argv, missing profile, sticky
  • tests/hermes_cli/test_tui_resume_flow.py — exit 43 relaunch ± resume
  • tests/test_tui_gateway_server.py -k profile_switch
  • shared helper unit coverage for resume present/absent

Ready for another pass whenever convenient.

@AnkitArya

Copy link
Copy Markdown
Author

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 /profile stays status-only.

Contract we are proposing (KISS):

  • /profile → status (shared executor)
  • /profile <name> → sticky select + process relaunch under that profile
  • New: relaunch includes --resume for the target profile’s last session when one exists (workspace then global MRU)
  • Not: carry the current live transcript across profiles or mutate toolsets mid-turn

Benefits of keeping it simple:

  • Reuses --profile + --resume instead of a second live-swap subsystem
  • Honors profile isolation and prompt-cache rules
  • Smaller diff, fewer edge cases, clearer tests than mid-session bundle swap
  • Still unblocks the designer↔coder workflow: switch profile and land on that mode’s last chat

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.

@alt-glitch alt-glitch removed sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Aug 11, 2026
@AnkitArya
AnkitArya requested a review from teknium1 August 18, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping area/sessions Session lifecycle, resume, persistence, history comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: switch the whole profile (model + toolset + persona) mid-session with /profile <name>

4 participants