mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 14:14:57 +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 |
|
| `/stream` `/verbose` `/tools` `/rounds` | Toggles and limits |
|
||||||
| `/retry` | Re-run incomplete last user turn (after error/cancel) |
|
| `/retry` | Re-run incomplete last user turn (after error/cancel) |
|
||||||
| `/provider` `/model` | Switch without restarting |
|
| `/provider` `/model` | Switch without restarting |
|
||||||
|
| `/config` | Edit `plyngent.toml` in `$EDITOR` and reload |
|
||||||
| `/quit` | Leave the REPL |
|
| `/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.
|
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")
|
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")
|
@slash.command("status")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def status_cmd(state: ReplState) -> None:
|
def status_cmd(state: ReplState) -> None:
|
||||||
|
|||||||
@@ -111,6 +111,39 @@ class ReplState:
|
|||||||
self.agent.messages = messages
|
self.agent.messages = messages
|
||||||
self.sync_display_flags()
|
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:
|
def _set_workspace(self, path: Path) -> None:
|
||||||
"""Update REPL + tool workspace root."""
|
"""Update REPL + tool workspace root."""
|
||||||
resolved = path.expanduser().resolve()
|
resolved = path.expanduser().resolve()
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ class ConfigStore:
|
|||||||
self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {})))
|
self._agent = _parse_agent(cast("dict[str, object]", raw.get("agent", {})))
|
||||||
self._providers, self._bad_providers = _parse_providers(document)
|
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) --
|
# -- database (read-only) --
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -173,6 +173,26 @@ async def test_rename_slash(state: ReplState) -> None:
|
|||||||
assert row.name == "my-chat"
|
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:
|
async def test_model_switch_persists(state: ReplState) -> None:
|
||||||
from plyngent.config.models import ModelConfig
|
from plyngent.config.models import ModelConfig
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user