refactor(pets): unify the /pet slash command; drop /pets
/pet now toggles, browses (/pet list), adopts (/pet <slug>), and resizes (/pet scale <n>) across CLI, TUI, and desktop; the separate /pets command is removed. CLI pet-state derivation delegates to agent.pet.state.derive_pet_state so the surfaces can't drift. Adds set_pet_scale/toggle/gallery helpers + `hermes pets scale`.
This commit is contained in:
parent
b6abd39ca5
commit
52134078e3
@ -52,6 +52,16 @@ describe('desktop slash command curation', () => {
|
||||
expect(desktopSlashUnavailableMessage('/personality')).toBeNull()
|
||||
})
|
||||
|
||||
it('routes /pet through the desktop action handler and drops /pets', () => {
|
||||
expect(resolveDesktopCommand('/pet')?.surface).toEqual({ kind: 'action', action: 'pet' })
|
||||
expect(resolveDesktopCommand('/pet')?.args).toBe(true)
|
||||
expect(isDesktopSlashSuggestion('/pet')).toBe(true)
|
||||
expect(isDesktopSlashCommand('/pet')).toBe(true)
|
||||
expect(resolveDesktopCommand('/pets')?.surface).toEqual({ kind: 'unavailable', reason: 'settings' })
|
||||
expect(isDesktopSlashSuggestion('/pets')).toBe(false)
|
||||
expect(isDesktopSlashCommand('/pets')).toBe(false)
|
||||
})
|
||||
|
||||
it('treats /browser as an executable action command (local-gateway connect)', () => {
|
||||
// /browser used to be terminal-only; it now resolves to a desktop action
|
||||
// handler that routes browser.manage RPC when the gateway is local.
|
||||
|
||||
@ -34,6 +34,7 @@ export type DesktopActionId =
|
||||
| 'handoff'
|
||||
| 'help'
|
||||
| 'new'
|
||||
| 'pet'
|
||||
| 'profile'
|
||||
| 'skin'
|
||||
| 'title'
|
||||
@ -128,8 +129,7 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [
|
||||
{ name: '/debug', description: 'Create a debug report', surface: exec() },
|
||||
{ name: '/goal', description: 'Manage the standing goal for this session', surface: exec() },
|
||||
{ name: '/personality', description: 'Switch personality for this session', surface: exec(), args: true },
|
||||
{ name: '/pet', description: 'Bring out a petdex mascot (/pet boba, /pet off)', surface: exec(), args: true },
|
||||
{ name: '/pets', description: 'List your pets, or browse the petdex gallery', surface: exec(), args: true },
|
||||
{ name: '/pet', description: 'Toggle or adopt a petdex mascot (/pet, /pet list, /pet boba)', surface: action('pet'), args: true },
|
||||
{ name: '/queue', description: 'Queue a prompt for the next turn', aliases: ['/q'], surface: exec() },
|
||||
{ name: '/retry', description: 'Retry the last user message', surface: exec() },
|
||||
{ name: '/rollback', description: 'List or restore filesystem checkpoints', surface: exec() },
|
||||
@ -157,7 +157,7 @@ const NO_DESKTOP_SURFACE: Record<DesktopUnavailableReason, readonly string[]> =
|
||||
'/sb', '/set-home', '/sethome', '/snap', '/snapshot', '/statusbar', '/toolsets', '/update', '/verbose'
|
||||
],
|
||||
messaging: ['/approve', '/deny'],
|
||||
settings: ['/skills'],
|
||||
settings: ['/skills', '/pets'],
|
||||
advanced: ['/curator', '/fast', '/insights', '/kanban', '/reasoning', '/voice']
|
||||
}
|
||||
|
||||
|
||||
19
cli.py
19
cli.py
@ -4222,17 +4222,22 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||
self._pet_flash("jump" if done else "wave")
|
||||
|
||||
def _derive_pet_state(self) -> str:
|
||||
"""Map current CLI activity to a pet animation state (mirrors the TUI).
|
||||
"""Map current CLI activity to a pet animation state.
|
||||
|
||||
A transient reaction beat wins while it's live; otherwise reasoning →
|
||||
``review``, an in-flight turn → ``run``, idle at rest.
|
||||
A transient reaction beat (wave/jump/failed) wins while it's live;
|
||||
otherwise the steady state comes from the shared
|
||||
:func:`agent.pet.state.derive_pet_state` so the CLI can't drift from the
|
||||
TUI/desktop priority order.
|
||||
"""
|
||||
if self._pet_event and time.monotonic() < self._pet_event_until:
|
||||
return self._pet_event
|
||||
self._pet_event = ""
|
||||
if getattr(self, "_agent_running", False):
|
||||
return "review" if self._pet_reasoning else "run"
|
||||
return "idle"
|
||||
from agent.pet.state import derive_pet_state
|
||||
|
||||
return derive_pet_state(
|
||||
busy=getattr(self, "_agent_running", False),
|
||||
reasoning=self._pet_reasoning,
|
||||
).value
|
||||
|
||||
def _pet_frames_for(self, state: str) -> list:
|
||||
"""Return (and cache) the half-block grids for one state."""
|
||||
@ -7650,8 +7655,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
||||
self._handle_personality_command(cmd_original)
|
||||
elif canonical == "pet":
|
||||
self._handle_pet_command(cmd_original)
|
||||
elif canonical == "pets":
|
||||
self._handle_pets_command(cmd_original)
|
||||
elif canonical == "retry":
|
||||
retry_msg = self.retry_last()
|
||||
if retry_msg and hasattr(self, '_pending_input'):
|
||||
|
||||
@ -1040,29 +1040,47 @@ class CLICommandsMixin:
|
||||
print()
|
||||
|
||||
def _handle_pet_command(self, cmd: str):
|
||||
"""Bring out a petdex mascot (selection only — see ``/pets`` for the list).
|
||||
"""Toggle, browse, or adopt a petdex mascot.
|
||||
|
||||
``/pet <slug>`` → adopt (install if needed) + make active
|
||||
``/pet off`` → put the pet away
|
||||
``/pet`` → show the active pet + a pointer to ``/pets``
|
||||
``/pet`` / ``/pet toggle`` → flip ``display.pet.enabled`` on/off
|
||||
``/pet list`` → browse the petdex gallery
|
||||
``/pet scale <n>`` → resize the pet everywhere (e.g. 0.5)
|
||||
``/pet <slug>`` → adopt (install if needed) + make active
|
||||
``/pet off`` → disable (alias for toggle-off)
|
||||
|
||||
Writes ``display.pet.*`` to config; the CLI/TUI/desktop pet surfaces
|
||||
pick the change up on their next poll, so the pet appears shortly.
|
||||
"""
|
||||
from agent.pet import store
|
||||
from agent.pet.manifest import ManifestError
|
||||
from hermes_cli.pets import _pet_config, _set_active, _set_enabled
|
||||
from hermes_cli.pets import _set_active, _set_enabled, print_pet_gallery, set_pet_scale, toggle_pet_display
|
||||
|
||||
parts = cmd.split(maxsplit=1)
|
||||
arg = parts[1].strip() if len(parts) > 1 else ""
|
||||
low = arg.lower()
|
||||
|
||||
if not arg:
|
||||
cfg = _pet_config()
|
||||
active = store.resolve_active_pet(str(cfg.get("slug", "") or ""))
|
||||
state = "on" if cfg.get("enabled") else "off"
|
||||
print(f"(^_^) Pet: {state} · active: {active.display_name if active else 'none'}")
|
||||
print(" /pet <slug> to bring one out · /pet off · /pets for your collection")
|
||||
if not arg or low == "toggle":
|
||||
enabled, name, err = toggle_pet_display()
|
||||
if err:
|
||||
print(f"(x_x) {err}")
|
||||
return
|
||||
if enabled:
|
||||
print(f"(^_^)b {name} is out — it'll pop in shortly.")
|
||||
else:
|
||||
print(f"(-_-)zzZ {name} put away." if name else "(-_-)zzZ Pet put away.")
|
||||
return
|
||||
|
||||
if low in ("list", "gallery", "browse", "all"):
|
||||
print_pet_gallery()
|
||||
return
|
||||
|
||||
if low == "scale" or low.startswith("scale "):
|
||||
value = arg[len("scale"):].strip()
|
||||
if not value:
|
||||
print("(o_o) Usage: /pet scale <factor> (e.g. /pet scale 0.5)")
|
||||
return
|
||||
scale, err = set_pet_scale(value)
|
||||
print(f"(x_x) {err}" if err else f"(^_^) Pet scale → {scale:g}.")
|
||||
return
|
||||
|
||||
if low == "off":
|
||||
@ -1079,48 +1097,6 @@ class CLICommandsMixin:
|
||||
_set_active(arg)
|
||||
print(f"(^_^)b {pet.display_name} is out — it'll pop in shortly.")
|
||||
|
||||
def _handle_pets_command(self, cmd: str):
|
||||
"""List your pets, or browse the petdex gallery.
|
||||
|
||||
``/pets`` → your installed pets (active one marked)
|
||||
``/pets gallery`` → browse the petdex catalog (first 20)
|
||||
"""
|
||||
from agent.pet import store
|
||||
from agent.pet.manifest import ManifestError, fetch_manifest
|
||||
from hermes_cli.pets import _pet_config
|
||||
|
||||
parts = cmd.split(maxsplit=1)
|
||||
arg = parts[1].strip().lower() if len(parts) > 1 else ""
|
||||
|
||||
if arg in ("gallery", "all", "browse"):
|
||||
try:
|
||||
entries = fetch_manifest()
|
||||
except ManifestError as exc:
|
||||
print(f"(._.) Couldn't reach the petdex gallery: {exc}")
|
||||
return
|
||||
installed = {p.slug for p in store.installed_pets()}
|
||||
print(f"(^o^)/ petdex gallery — first 20 of {len(entries)}:")
|
||||
for entry in entries[:20]:
|
||||
mark = "●" if entry.slug in installed else "○"
|
||||
print(f" {mark} {entry.slug:<24} {entry.display_name}")
|
||||
print(" /pet <slug> to bring one out")
|
||||
return
|
||||
|
||||
cfg = _pet_config()
|
||||
active = store.resolve_active_pet(str(cfg.get("slug", "") or ""))
|
||||
active_slug = active.slug if active else ""
|
||||
installed = store.installed_pets()
|
||||
state = "on" if cfg.get("enabled") else "off"
|
||||
|
||||
print(f"(^_^) Your pets ({state}):")
|
||||
if not installed:
|
||||
print(" none yet — /pet boba to adopt one · /pets gallery to browse")
|
||||
return
|
||||
for p in installed:
|
||||
mark = "→" if p.slug == active_slug else " "
|
||||
print(f" {mark} {p.slug:<24} {p.display_name}")
|
||||
print(" /pet <slug> to switch · /pet off · /pets gallery to browse")
|
||||
|
||||
def _handle_cron_command(self, cmd: str):
|
||||
"""Handle the /cron command to manage scheduled tasks."""
|
||||
from cli import get_job
|
||||
|
||||
@ -176,10 +176,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
||||
subcommands=("pending", "approve", "reject", "approval")),
|
||||
CommandDef("bundles", "List skill bundles (aliases /<name> for multiple skills)",
|
||||
"Tools & Skills"),
|
||||
CommandDef("pet", "Bring out a petdex mascot (/pet <slug>, /pet off)", "Tools & Skills",
|
||||
cli_only=True, args_hint="<slug|off>", subcommands=("off",)),
|
||||
CommandDef("pets", "List your pets, or browse the petdex gallery", "Tools & Skills",
|
||||
cli_only=True, args_hint="[gallery]", subcommands=("gallery",)),
|
||||
CommandDef("pet", "Toggle or adopt a petdex mascot (/pet, /pet list, /pet <slug>)", "Tools & Skills",
|
||||
cli_only=True, args_hint="[toggle|list|scale <n>|<slug>]", subcommands=("toggle", "list", "scale", "off")),
|
||||
CommandDef("cron", "Manage scheduled tasks", "Tools & Skills",
|
||||
cli_only=True, args_hint="[subcommand]",
|
||||
subcommands=("list", "add", "create", "edit", "pause", "resume", "run", "remove")),
|
||||
|
||||
@ -127,6 +127,16 @@ def _cmd_off(args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_scale(args) -> int:
|
||||
"""Persist ``display.pet.scale`` — one knob resizes every surface."""
|
||||
scale, err = set_pet_scale(args.factor)
|
||||
if err:
|
||||
_err(f"✗ {err}")
|
||||
return 1
|
||||
_print(f"✓ pet scale set to {scale:g} (display.pet.scale)")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_show(args) -> int:
|
||||
"""Animate the active (or named) pet in the terminal.
|
||||
|
||||
@ -314,6 +324,81 @@ def _set_enabled(enabled: bool) -> None:
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def _set_scale(scale: float) -> None:
|
||||
from hermes_cli.config import load_config, save_config
|
||||
|
||||
cfg = load_config()
|
||||
display = cfg.setdefault("display", {})
|
||||
pet = display.setdefault("pet", {})
|
||||
pet["scale"] = scale
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def set_pet_scale(value: float | str) -> tuple[float, str | None]:
|
||||
"""Set ``display.pet.scale`` (clamped to bounds). Returns ``(applied, error)``.
|
||||
|
||||
The single write path behind ``/pet scale`` and the desktop slider, so every
|
||||
surface that resolves scale from config picks it up identically. *error* is
|
||||
set (and nothing written) only when *value* isn't a number.
|
||||
"""
|
||||
from agent.pet.constants import clamp_scale
|
||||
|
||||
try:
|
||||
scale = clamp_scale(float(value))
|
||||
except (TypeError, ValueError):
|
||||
return 0.0, f"not a number: {value!r} — try a value like 0.5"
|
||||
|
||||
_set_scale(scale)
|
||||
return scale, None
|
||||
|
||||
|
||||
def toggle_pet_display() -> tuple[bool, str | None, str | None]:
|
||||
"""Toggle ``display.pet.enabled``.
|
||||
|
||||
Returns ``(enabled, display_name, error_message)``. *error_message* is set
|
||||
when turning on but nothing is installed to show.
|
||||
"""
|
||||
from agent.pet import store
|
||||
|
||||
cfg = _pet_config()
|
||||
slug = str(cfg.get("slug", "") or "")
|
||||
pet = store.resolve_active_pet(slug)
|
||||
|
||||
if bool(cfg.get("enabled")):
|
||||
_set_enabled(False)
|
||||
return False, pet.display_name if pet else None, None
|
||||
|
||||
if pet is None:
|
||||
installed = store.installed_pets()
|
||||
if not installed:
|
||||
return False, None, "no pets installed — /pet list to browse, or /pet <slug> to adopt"
|
||||
pet = installed[0]
|
||||
_set_active(pet.slug)
|
||||
else:
|
||||
_set_enabled(True)
|
||||
return True, pet.display_name, None
|
||||
|
||||
|
||||
def print_pet_gallery(*, limit: int = 20) -> None:
|
||||
"""Print a slice of the public petdex gallery (CLI/TUI text fallback)."""
|
||||
from agent.pet import store
|
||||
from agent.pet.manifest import ManifestError, fetch_manifest
|
||||
|
||||
try:
|
||||
entries = fetch_manifest()
|
||||
except ManifestError as exc:
|
||||
print(f"(._.) Couldn't reach the petdex gallery: {exc}")
|
||||
return
|
||||
|
||||
installed = {p.slug for p in store.installed_pets()}
|
||||
shown = entries[:limit] if limit > 0 else entries
|
||||
print(f"(^o^)/ petdex gallery — first {len(shown)} of {len(entries)}:")
|
||||
for entry in shown:
|
||||
mark = "●" if entry.slug in installed else "○"
|
||||
print(f" {mark} {entry.slug:<24} {entry.display_name}")
|
||||
print(" /pet <slug> to adopt · /pet to toggle")
|
||||
|
||||
|
||||
def _clear_active_if(slug: str) -> bool:
|
||||
"""Disable + unset the active pet iff it's ``slug`` (e.g. after removal).
|
||||
|
||||
@ -384,6 +469,10 @@ def register_cli(parent: argparse.ArgumentParser) -> None:
|
||||
|
||||
subs.add_parser("off", help="Disable the pet display").set_defaults(func=_cmd_off)
|
||||
|
||||
p_scale = subs.add_parser("scale", help="Resize the pet everywhere (display.pet.scale)")
|
||||
p_scale.add_argument("factor", help="Scale factor, e.g. 0.5 (clamped 0.1–3.0)")
|
||||
p_scale.set_defaults(func=_cmd_scale)
|
||||
|
||||
p_remove = subs.add_parser("remove", help="Delete an installed pet")
|
||||
p_remove.add_argument("slug", help="Pet slug")
|
||||
p_remove.set_defaults(func=_cmd_remove)
|
||||
|
||||
@ -55,6 +55,10 @@ def _make_cli():
|
||||
cli_obj._pet_frames_cache = {}
|
||||
cli_obj._pet_frame_idx = 0
|
||||
cli_obj._agent_running = False
|
||||
# Transient-beat + reasoning state (set by HermesCLI.__init__ in production).
|
||||
cli_obj._pet_event = ""
|
||||
cli_obj._pet_event_until = 0.0
|
||||
cli_obj._pet_reasoning = False
|
||||
return cli_obj
|
||||
|
||||
|
||||
|
||||
104
tests/hermes_cli/test_pet_toggle.py
Normal file
104
tests/hermes_cli/test_pet_toggle.py
Normal file
@ -0,0 +1,104 @@
|
||||
"""Tests for pet slash-command config helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.pet import store
|
||||
from agent.pet.constants import FRAME_H, FRAME_W
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def boba_installed(tmp_path, monkeypatch):
|
||||
from PIL import Image
|
||||
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
|
||||
sheet = Image.new("RGBA", (FRAME_W * 8, FRAME_H * 9), (0, 0, 0, 0))
|
||||
pet_dir = store.pets_dir() / "boba"
|
||||
pet_dir.mkdir(parents=True, exist_ok=True)
|
||||
sheet.save(pet_dir / "spritesheet.webp")
|
||||
(pet_dir / "pet.json").write_text(
|
||||
'{"id":"boba","displayName":"Boba","description":"d","spritesheetPath":"spritesheet.webp"}'
|
||||
)
|
||||
return home
|
||||
|
||||
|
||||
def _write_config(home, *, enabled: bool, slug: str = "") -> None:
|
||||
import yaml
|
||||
|
||||
cfg = {"display": {"pet": {"enabled": enabled, "slug": slug, "scale": 0.33}}}
|
||||
(home / "config.yaml").write_text(yaml.dump(cfg), encoding="utf-8")
|
||||
|
||||
|
||||
def test_toggle_pet_display_turns_off_when_enabled(boba_installed):
|
||||
from hermes_cli.pets import _pet_config, toggle_pet_display
|
||||
|
||||
_write_config(boba_installed, enabled=True, slug="boba")
|
||||
|
||||
enabled, name, err = toggle_pet_display()
|
||||
|
||||
assert err is None
|
||||
assert enabled is False
|
||||
assert name == "Boba"
|
||||
assert _pet_config()["enabled"] is False
|
||||
|
||||
|
||||
def test_toggle_pet_display_turns_on_resolved_pet(boba_installed):
|
||||
from hermes_cli.pets import _pet_config, toggle_pet_display
|
||||
|
||||
_write_config(boba_installed, enabled=False, slug="boba")
|
||||
|
||||
enabled, name, err = toggle_pet_display()
|
||||
|
||||
assert err is None
|
||||
assert enabled is True
|
||||
assert name == "Boba"
|
||||
assert _pet_config()["enabled"] is True
|
||||
|
||||
|
||||
def test_toggle_pet_display_errors_with_no_installed_pets(tmp_path, monkeypatch):
|
||||
from hermes_cli.pets import toggle_pet_display
|
||||
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
_write_config(home, enabled=False, slug="")
|
||||
|
||||
enabled, name, err = toggle_pet_display()
|
||||
|
||||
assert enabled is False
|
||||
assert name is None
|
||||
assert err is not None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def empty_home(tmp_path, monkeypatch):
|
||||
home = tmp_path / ".hermes"
|
||||
home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||
return home
|
||||
|
||||
|
||||
def test_set_pet_scale_writes_clamped_value(empty_home):
|
||||
from agent.pet.constants import MAX_SCALE, MIN_SCALE
|
||||
from hermes_cli.pets import _pet_config, set_pet_scale
|
||||
|
||||
applied, err = set_pet_scale("0.5")
|
||||
assert err is None
|
||||
assert applied == 0.5
|
||||
assert _pet_config()["scale"] == 0.5
|
||||
|
||||
# Out-of-range values clamp to the bounds rather than erroring.
|
||||
assert set_pet_scale(99) == (MAX_SCALE, None)
|
||||
assert set_pet_scale(0) == (MIN_SCALE, None)
|
||||
|
||||
|
||||
def test_set_pet_scale_rejects_non_numbers(empty_home):
|
||||
from hermes_cli.pets import set_pet_scale
|
||||
|
||||
applied, err = set_pet_scale("huge")
|
||||
assert applied == 0.0
|
||||
assert err is not None
|
||||
@ -208,17 +208,28 @@ describe('createSlashHandler', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('opens the pet picker locally for bare /pet and /pet list', () => {
|
||||
it('opens the pet picker for /pet list only', () => {
|
||||
const ctx = buildCtx()
|
||||
|
||||
expect(createSlashHandler(ctx)('/pet')).toBe(true)
|
||||
expect(createSlashHandler(ctx)('/pet list')).toBe(true)
|
||||
expect(getOverlayState().petPicker).toBe(true)
|
||||
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||
|
||||
resetOverlayState()
|
||||
expect(createSlashHandler(ctx)('/pet list')).toBe(true)
|
||||
expect(getOverlayState().petPicker).toBe(true)
|
||||
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||
expect(createSlashHandler(ctx)('/pet')).toBe(true)
|
||||
expect(getOverlayState().petPicker).toBe(false)
|
||||
expect(ctx.gateway.gw.request).toHaveBeenCalledWith(
|
||||
'slash.exec',
|
||||
expect.objectContaining({ command: 'pet' })
|
||||
)
|
||||
|
||||
resetOverlayState()
|
||||
expect(createSlashHandler(ctx)('/pet toggle')).toBe(true)
|
||||
expect(getOverlayState().petPicker).toBe(false)
|
||||
expect(ctx.gateway.gw.request).toHaveBeenCalledWith(
|
||||
'slash.exec',
|
||||
expect.objectContaining({ command: 'pet toggle' })
|
||||
)
|
||||
})
|
||||
|
||||
it('routes /pet <slug> to the slash worker without opening the picker', () => {
|
||||
|
||||
@ -342,19 +342,18 @@ export const sessionCommands: SlashCommand[] = [
|
||||
},
|
||||
|
||||
{
|
||||
help: 'pick / adopt an animated pet',
|
||||
help: 'toggle / adopt / resize an animated pet',
|
||||
name: 'pet',
|
||||
usage: '/pet [list | <slug> | off]',
|
||||
usage: '/pet [toggle | list | scale <n> | <slug>]',
|
||||
run: (arg, ctx, cmd) => {
|
||||
const sub = arg.trim().toLowerCase()
|
||||
|
||||
// No slug (or an explicit "list") → the interactive picker; the TUI can
|
||||
// do this even though the text `/pet` path only prints. status/off/<slug>
|
||||
// keep their text behaviour via the slash worker.
|
||||
if (!sub || sub === 'list') {
|
||||
// Gallery picker — the interactive browse surface.
|
||||
if (sub === 'list') {
|
||||
return patchOverlayState({ petPicker: true })
|
||||
}
|
||||
|
||||
// Bare /pet and /pet toggle flip display.pet.enabled via the slash worker.
|
||||
ctx.gateway.gw
|
||||
.request<SlashExecResponse>('slash.exec', { command: cmd.slice(1), session_id: ctx.sid })
|
||||
.then(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user