mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/cli: /config opens plyngent.toml and reloads
Add slash /config to edit the active config path in $EDITOR, then reload providers/agent settings while keeping a valid provider/model selection.
This commit is contained in:
@@ -124,6 +124,7 @@ Type `/help` in the REPL for the live list. Common ones:
|
||||
| `/stream` `/verbose` `/tools` `/rounds` | Toggles and limits |
|
||||
| `/retry` | Re-run incomplete last user turn (after error/cancel) |
|
||||
| `/provider` `/model` | Switch without restarting |
|
||||
| `/config` | Edit `plyngent.toml` in `$EDITOR` and reload |
|
||||
| `/quit` | Leave the REPL |
|
||||
|
||||
User messages are saved immediately. On API error or Ctrl+C, partial assistant/tool output is discarded but the user message stays so `/retry` works after resume. Interactive auto-retry uses 10s / 20s / 30s delays.
|
||||
|
||||
@@ -293,6 +293,35 @@ def edit_cmd(state: ReplState) -> None:
|
||||
click.echo(f"(edit) {len(text)} characters ready to send")
|
||||
|
||||
|
||||
@slash.command("config")
|
||||
@click.pass_obj
|
||||
def config_cmd(state: ReplState) -> None:
|
||||
"""Open plyngent.toml in ``$EDITOR``, then reload providers/agent settings.
|
||||
|
||||
Same file as ``plyngent config edit``. After the editor exits, config is
|
||||
re-read; current provider/model are kept when still valid.
|
||||
"""
|
||||
from plyngent import config as config_mod
|
||||
from plyngent.cli.editor import open_in_editor
|
||||
|
||||
path = state.config.path
|
||||
try:
|
||||
open_in_editor(path)
|
||||
except click.ClickException as exc:
|
||||
click.echo(f"error: {exc}")
|
||||
return
|
||||
try:
|
||||
state.reload_config_from_disk()
|
||||
except (config_mod.ConfigFormatError, ValueError, OSError) as exc:
|
||||
click.secho(f"error: config reload failed: {exc}", fg="red")
|
||||
click.echo(f"config file: {path}")
|
||||
return
|
||||
if state.config.bad_providers:
|
||||
names = ", ".join(sorted(state.config.bad_providers.keys()))
|
||||
click.secho(f"warning: ignored bad providers: {names}", fg="yellow")
|
||||
click.echo(f"config reloaded from {path}\nprovider={state.provider_name} model={state.model}")
|
||||
|
||||
|
||||
@slash.command("status")
|
||||
@click.pass_obj
|
||||
def status_cmd(state: ReplState) -> None:
|
||||
|
||||
@@ -111,6 +111,39 @@ class ReplState:
|
||||
self.agent.messages = messages
|
||||
self.sync_display_flags()
|
||||
|
||||
def reload_config_from_disk(self) -> None:
|
||||
"""Re-read TOML config and re-bind provider/model when still valid."""
|
||||
import click
|
||||
|
||||
from plyngent.cli.selection import select_model, select_provider
|
||||
from plyngent.runtime import ProviderNotSupportedError
|
||||
from plyngent.tools import set_path_denylist
|
||||
|
||||
self.config.reload()
|
||||
set_path_denylist(self.config.agent_config.path_denylist or None)
|
||||
|
||||
preferred_provider = self.provider_name if self.provider_name in self.config.providers else None
|
||||
preferred_model = self.model
|
||||
try:
|
||||
pname, provider = select_provider(
|
||||
self.config.providers,
|
||||
preferred=preferred_provider,
|
||||
interactive=False,
|
||||
)
|
||||
except (click.ClickException, ProviderNotSupportedError) as exc:
|
||||
msg = f"config reloaded but provider selection failed: {exc}"
|
||||
raise ValueError(msg) from exc
|
||||
try:
|
||||
model_id = select_model(provider, preferred=preferred_model, interactive=False)
|
||||
except click.ClickException:
|
||||
# Previous model not on this provider; pick first listed or keep free-form.
|
||||
model_id = next(iter(sorted(provider.models.keys()))) if provider.models else preferred_model
|
||||
|
||||
self.provider_name = pname
|
||||
self.provider = provider
|
||||
self.model = model_id
|
||||
self.rebuild_client()
|
||||
|
||||
def _set_workspace(self, path: Path) -> None:
|
||||
"""Update REPL + tool workspace root."""
|
||||
resolved = path.expanduser().resolve()
|
||||
|
||||
@@ -99,6 +99,11 @@ class ConfigStore:
|
||||
self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {})))
|
||||
self._providers, self._bad_providers = _parse_providers(document)
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
"""Filesystem path of the TOML config file."""
|
||||
return self._path
|
||||
|
||||
# -- database (read-only) --
|
||||
|
||||
@property
|
||||
|
||||
@@ -173,6 +173,26 @@ async def test_rename_slash(state: ReplState) -> None:
|
||||
assert row.name == "my-chat"
|
||||
|
||||
|
||||
async def test_config_slash_reloads(
|
||||
state: ReplState,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
path = state.config.path
|
||||
assert path is not None
|
||||
# Fixture ConfigStore may not have written the file yet.
|
||||
state.config.write()
|
||||
|
||||
def fake_open(p: object, **_k: object) -> None:
|
||||
assert str(p) == str(path)
|
||||
|
||||
monkeypatch.setattr("plyngent.cli.editor.open_in_editor", fake_open)
|
||||
assert await handle_slash(state, "/config") is True
|
||||
out = capsys.readouterr().out
|
||||
assert "config reloaded" in out
|
||||
assert state.provider_name
|
||||
|
||||
|
||||
async def test_model_switch_persists(state: ReplState) -> None:
|
||||
from plyngent.config.models import ModelConfig
|
||||
|
||||
|
||||
Reference in New Issue
Block a user