NousResearch/hermes-agent

Inline pickers for /reasoning and /fast commands

Cluster 30582 · gateway. Data as of

2 open items. Fix merged in #65799 on 16 Jul 2026. Check it covers your case.

The short answer

1open issue
1open PR
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #65799. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #65799. That is separate from the fix event.
Still open
1 issue and 1 PR.

These members relate to adding inline keyboard/picker UI elements for the /reasoning and /fast slash commands across chat platforms, particularly Telegram, bringing them to parity with the existing /model picker.

Open issues1

  • Issue#30582Feature: Telegram inline keyboard buttons for slash commands with discrete options

Open PRs1

  • PR#64026feat(telegram): add reasoning and fast pickers

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #65799 feat(gateway): inline choice pickers for /reasoning and /fast across Telegram, Discord, Matrix

    merged to main

    files: gateway/platforms/ADDING_A_PLATFORM.md, gateway/slash_commands.py, locales/af.yaml, locales/de.yaml, locales/en.yaml, locales/es.yaml, locales/fr.yaml, locales/ga.yaml, locales/hu.yaml, locales/it.yaml, locales/ja.yaml, locales/ko.yaml, locales/pt.yaml, locales/ru.yaml, locales/tr.yaml, locales/uk.yaml, locales/zh-hant.yaml, locales/zh.yaml, plugins/platforms/discord/adapter.py, plugins/platforms/matrix/adapter.py (+2 more)

    Show the diff
    • gateway/platforms/ADDING_A_PLATFORM.md
      @@ -120,6 +120,7 @@ If your platform supports interactive button/menu messages, implement these for
       | `send_exec_approval(chat_id, command, session_key, description, ...)` | Render dangerous-command approval as Approve/Deny buttons. Inbound dispatch routes to `tools.approval.resolve_gateway_approval`. |
       | `send_slash_confirm(chat_id, title, message, session_key, confirm_id, ...)` | Render slash-command confirmations (e.g. `/reload-mcp`) as Once/Always/Cancel buttons. Inbound dispatch routes to `tools.slash_confirm.resolve`. |
       | `send_model_picker(...)` | Interactive `/model` picker. Used by Telegram and Discord. |
      +| `send_choice_picker(...)` | Flat single-level picker for finite-choice commands (`/reasoning`, `/fast`). Implemented by Telegram (inline keyboard), Discord (select menu), and Matrix (reactions). Platforms without it fall back to the text status card automatically. |
       
       See `gateway/platforms/telegram.py`, `discord.py`, and `whatsapp_cloud.py` for reference implementations. The button-callback id convention (`cl:<id>:<idx>`, `appr:<id>:<choice>`, `sc:<choice>:<id>`) is shared across adapters — match it so the gateway-side resolvers work without modification.
       
    • gateway/slash_commands.py
      @@ -2704,7 +2704,154 @@ async def _handle_background_command(self, event: MessageEvent) -> str:
               preview = prompt[:60] + ("..." if len(prompt) > 60 else "")
               return t("gateway.background.started", preview=preview, task_id=task_id)
       
      -    async def _handle_reasoning_command(self, event: MessageEvent) -> str:
      +    def _save_gateway_config_key(self, key_path: str, value) -> bool:
      +        """Save a dot-separated key to config.yaml (shared by /reasoning, /fast
      +        and their interactive pickers)."""
      +        import yaml
      +        from gateway.run import _hermes_home
      +        config_path = _hermes_home / "config.yaml"
      +        try:
      +            user_config = {}
      +            if config_path.exists():
      +                with open(config_path, encoding="utf-8") as f:
      +                    user_config = yaml.safe_load(f) or {}
      +            keys = key_path.split(".")
      +            current = user_config
      +            for k in keys[:-1]:
      +                if k not in current or not isinstance(current[k], dict):
      +                    current[k] = {}
      +                current = current[k]
      +            current[keys[-1]] = value
      +            atomic_config_write(config_path, user_config)
      +            return True
      +        except Exception as e:
      +            logger.error("Failed to save config key %s: %s", key_path, e)
      +            return False
      +
      +    def _apply_reasoning_selection(
      +        self,
      +        session_key: str,
      +        platform_key: str,
      +        value: str,
      +        persist_global: bool = False,
      +    ) -> str:
      +        """Apply a /reasoning argument (typed or picked) and return the reply.
      +
      +        Single application path shared by the typed `/reasoning <arg>` branch
      +        and the interactive choice picker, so both surfaces stay in lockstep
      +        with the canonical parser.
      +        """
      +        from hermes_constants import parse_reasoning_effort
      +
      +        value = (value or "").strip().lower()
      +
      +        # Display toggle (per-platform)
      +        if value in {"show", "on"}:
      +            self._show_reasoning = True
      +            self._save_gateway_config_key(
      +                f"display.platforms.{platform_key}.show_reasoning", True
      +            )
      +            return t("gateway.reasoning.display_set_on", platform=platform_key)
      +        if value in {"hide", "off"}:
      +            self._show_reasoning = False
      +            self._save_gateway_config_key(
      +                f"display.platforms.{platform_key}.show_reasoning", False
      +            )
      +            return t("gateway.reasoning.display_set_off", platform=platform_key)
      +
      +        if value == "reset":
      +            if persist_global:
      +                return t("gateway.reasoning.reset_global_unsupported")
      +            self._set_session_reasoning_override(session_key, None)
      +            self._reasoning_config = self._load_reasoning_config()
      +            self._evict_cached_agent(session_key)
      +            return t("gateway.reasoning.reset_done")
      +
      +        parsed = parse_reasoning_effort(value)
      +        if parsed is None:
      +            return t("gateway.reasoning.unknown_arg", arg=value)
      +
      +        self._reasoning_config = parsed
      +        if persist_global:
      +            if self._save_gateway_config_key("agent.reasoning_effort", value):
      +                self._set_session_reasoning_override(session_key, None)
      +                self._evict_cached_agent(session_key)
      +                return t("gateway.reasoning.set_global", effort=value)
      +            self._set_session_reasoning_override(session_key, parsed)
      +            self._evict_cached_agent(session_key)
      +            return t("gateway.reasoning.set_global_save_failed", effort=value)
      +
      +        self._set_session_reasoning_override(session_key, parsed)
      +        self._evict_cached_agent(session_key)
      +        return t("gateway.reasoning.set_session", effort=value)
      +
      +    def _reasoning_picker_choices(self, current_effort: str) -> list:
      +        """Build the choice list for the interactive /reasoning picker."""
      +        from hermes_constants import VALID_REASONING_EFFORTS
      +
      +        choices = [
      +            {
      +                "value": "none",
      +                "label": t("gateway.reasoning.choice_none"),
      +                "is_current": current_effort == "none",
      +            }
      +        ]
      +        for level in VALID_REASONING_EFFORTS:
      +            choices.append(
      +                {
      +                    "value": level,
      +                    "label": level,
      +                    "is_current": level == current_effort,
      +                }
      +            )
      +        choices.extend(
      +            [
      +                {"value": "reset", "label": t("gateway.reasoning.choice_reset"), "is_current": False},
      +                {"value": "show", "label": t("gateway.reasoning.choice_show"), "is_current": False},
      +                {"value": "hide", "label": t("gateway.reasoning.choice_hide"), "is_current": False},
      +            ]
      +        )
      +        return choices
      +
      +    async def _try_send_choice_picker(
      +        self,
      +        event: MessageEvent,
      +        session_key: str,
      +        title: str,
      +        choices: list,
      +        on_choice_selected,
      +    ) -> bool:
      +        """Send an interactive choice picker when the platform supports it.
      +
      +        Mirrors the `/model` picker gate: the capability is detected on the
      +        adapter *type* (``send_choice_picker``), and a failed send falls back
      +        to the text path (returns False) instead of erroring the command.
      +        """
      +        adapter = getattr(self, "_adapter_for_source")(event.source)
      +        has_picker = (
      +            adapter is not None
      +            and getattr(type(adapter), "send_choice_picker", None) is not None
      +        )
      +        if not has_picker:
      +            return False
      +        try:
      +            metadata = self._thread_metadata_for_source(
      +                event.source, self._reply_anchor_for_event(event)
      +            )
      +            result = await adapter.send_choice_picker(
      +                chat_id=event.source.chat_id,
      +                title=title,
      +                choices=choices,
      +                session_key=session_key,
      +                on_choice_selected=on_choice_selected,
      +                metadata=metadata,
      +            )
      +            return bool(getattr(result, "success", False))
      +        except Exception as e:
      +            logger.warning("send_choice_picker failed, falling back to text: %s", e)
      +            return False
      +
      +    async def _handle_reasoning_command(self, event: MessageEvent) -> Optional[str]:
               """Handle /reasoning command — manage reasoning effort and display toggle.
       
               Usage:
      @@ -2715,13 +2862,10 @@ async def _handle_reasoning_command(self, event: MessageEvent) -> str:
                   /reasoning show|on               Show model reasoning in responses
                   /reasoning hide|off              Hide model reasoning from responses
               """
      -        from gateway.run import _hermes_home, _platform_config_key
      -        from hermes_constants import parse_reasoning_effort
      -        import yaml
      +        from gateway.run import _platform_config_key
       
               raw_args = event.get_command_args().strip()
               args, persist_global = self._parse_reasoning_command_args(raw_args)
      -        config_path = _hermes_home / "config.yaml"
               # Normalize the source (Telegram DM topic recovery) before deriving
               # the override key so storage matches the key the next message turn
               # reads — same fix as /model (#30479).
      @@ -2739,35 +2883,18 @@ async def _handle_reasoning_command(self, event: MessageEvent) -> str:
                   model=_session_model,
               )
       
      -        def _save_config_key(key_path: str, value):
      -            """Save a dot-separated key to config.yaml."""
      -            try:
      -                user_config = {}
      -                if config_path.exists():
      -                    with open(config_path, encoding="utf-8") as f:
      -                        user_config = yaml.safe_load(f) or {}
      -                keys = key_path.split(".")
      -                current = user_config
      -                for k in keys[:-1]:
      -                    if k not in current or not isinstance(current[k], dict):
      -                        current[k] = {}
      -                    current = current[k]
      -                current[keys[-1]] = value
      -                atomic_config_write(config_path, user_config)
      -                return True
      -            except Exception as e:
      -                logger.error("Failed to save config key %s: %s", key_path, e)
      -                return False
      -
               if not raw_args:
                   # Show current state
                   rc = self._reasoning_config
                   if rc is None:
                       level = t("gateway.reasoning.level_default")
      +                current_effort = "medium"
                   elif rc.get("enabled") is False:
                       level = t("gateway.reasoning.level_disabled")
      +                current_effort = "none"
                   else:
                       level = rc.get("effort", "medium")
      +                current_effort = level
                   display_state = (
                       t("gateway.reasoning.display_on")
                       if self._show_reasoning
      @@ -2779,6 +2906,31 @@ def _save_config_key(key_path: str, value):
                       if has_session_override
                       else t("gateway.reasoning.scope_global")
                   )
      +
      +            # Interactive picker on platforms that support it (parity with the
      +            # /model picker). Falls through to the text status card otherwise.
      +            _picker_platform_key = _platform_config_key(event.source.platform)
      +
      +            async def _on_reasoning_choice(_chat_id: str, value: str) -> str:
      +                return self._apply_reasoning_selection(
      +                    session_key, _picker_platform_key, value
      +                )
      +
      +            picker_sent = await self._try_send_choice_picker(
      +                event,
      +                session_key,
      +                title=t(
      +                    "gateway.reasoning.picker_title",
      +                    level=level,
      +                    scope=scope,
      +                    display=display_state,
      +                ),
      +                choices=self._reasoning_picker_choices(current_effort),
      +                on_choice_selected=_on_reasoning_choice,
      +            )
      +            if picker_sent:
      +                return None  # Picker sent — adapter handles the response
      +
                   return t(
                       "gateway.reasoning.status",
                       level=level,
      @@ -2786,47 +2938,11 @@ def _save_config_key(key_path: str, value):
                       display=display_state,
                   )
       
      -        # Display toggle (per-platform)
      +        # Typed argument path — same applier the picker uses.
               platform_key = _platform_config_key(event.source.platform)
      -        if args in {"show", "on"}:
      -            self._show_reasoning = True
      -            _save_config_key(f"display.platforms.{platform_key}.show_reasoning", True)
      -            return t("gateway.reasoning.display_set_on", platform=platform_key)
      -
      -        if args in {"hide", "off"}:
      -            self._show_reasoning = False
      -            _save_config_key(f"display.platforms.{platform_key}.show_reasoning", False)
      -            return t("gateway.reasoning.display_set_off", platform=platform_key)
      -
      -        # Effort level change
      -        effort = args.strip()
      -        if effort == "reset":
      -            if persist_global:
      -                return t("gateway.reasoning.reset_global_unsupported")
      -            self._set_session_reasoning_override(session_key, None)
      -            self._reasoning_config = self._load_reasoning_config()
      -            self._evict_cached_agent(session_key)
      -            return t("gateway.reasoning.reset_done")
      -        parsed = parse_reasoning_effort(effort)
      -        if parsed is None:
      -            return t(
      -                "gateway.reasoning.unknown_arg",
      -                arg=effort or raw_args.lower(),
      -            )
      -
      -        self._reasoning_config = parsed
      -        if persist_global:
      -            if _save_config_key("agent.reasoning_effort", effort):
      -                self._set_session_reasoning_override(session_key, None)
      -                self._evict_cached_agent(session_key)
      -                return t("gateway.reasoning.set_global", effort=effort)
      -            self._set_session_reasoning_override(session_key, parsed)
      -            self._evict_cached_agent(session_key)
      -            return t("gateway.reasoning.set_global_save_failed", effort=effort)
      -
      -        self._set_session_reasoning_override(session_key, parsed)
      -        self._evict_cached_agent(session_key)
      -        return t("gateway.reasoning.set_session", effort=effort)
      +        return self._apply_reasoning_selection(
      +            session_key, platform_key, args, persist_global=persist_global
      +        )
       
           async def _handle_memory_command(self, event: MessageEvent) -> str:
               """Handle /memory — review pending memory writes + toggle the approval gate.
      @@ -2932,14 +3048,12 @@ def _set_approval(enabled: bool):
                            f"~/.hermes/pending/skills/{pending_id}.json)")
               return out
       
      -    async def _handle_fast_command(self, event: MessageEvent) -> str:
      +    async def _handle_fast_command(self, event: MessageEvent) -> Optional[str]:
               """Handle /fast — mirror the CLI Priority Processing toggle in gateway chats."""
      -        from gateway.run import _hermes_home, _load_gateway_config, _resolve_gateway_model
      -        import yaml
      +        from gateway.run import _load_gateway_config, _resolve_gateway_model
               from hermes_cli.models import model_supports_fast_mode
       
               args = event.get_command_args().strip().lower()
      -        config_path = _hermes_home / "config.yaml"
               self._service_tier = self._load_service_tier()
       
               user_config = _load_gateway_config()
      @@ -2947,44 +3061,56 @@ async def _handle_fast_command(self, event: MessageEvent) -> str:
               if not model_supports_fast_mode(model):
                   return t("gateway.fast.not_supported")
       
      -        def _save_config_key(key_path: str, value):
      -            """Save a dot-separated key to config.yaml."""
      -            try:
      -                user_config = {}
      -                if config_path.exists():
      -                    with open(config_path, encoding="utf-8") as f:
      -                        user_config = yaml.safe_load(f) or {}
      -                keys = key_path.split(".")
      -                current = user_config
      -                for k in keys[:-1]:
      -                    if k not in current or not isinstance(current[k], dict):
      -                        current[k] = {}
      -                    current = current[k]
      -                current[keys[-1]] = value
      -                atomic_config_write(config_path, user_config)
      -                return True
      -            except Exception as e:
      -                logger.error("Failed to save config key %s: %s", key_path, e)
      -                return False
      +        def _apply_fast_selection(value: str) -> str:
      +            """Apply a /fast argument (typed or picked) and return the reply."""
      +            if value in {"fast", "on"}:
      +                self._service_tier = "priority"
      +                saved_value = "fast"
      +                label = t("gateway.fast.label_fast")
      +            elif value in {"normal", "off"}:
      +                self._service_tier = None
      +                saved_value = "normal"
      +                label = t("gateway.fast.label_normal")
      +            else:
      +                return t("gateway.fast.unknown_arg", arg=value)
      +            if self._save_gateway_config_key("agent.service_tier", saved_value):
      +                return t("gateway.fast.saved", label=label)
      +            return t("gateway.fast.session_only", label=label)
       
               if not args or args == "status":
      -            status = t("gateway.fast.status_fast") if self._service_tier == "priority" else t("gateway.fast.status_normal")
      -            return t("gateway.fast.status", mode=status)
      +            is_fast = self._service_tier == "priority"
      +            status = t("gateway.fast.status_fast") if is_fast else t("gateway.fast.status_normal")
       
      -        if args in {"fast", "on"}:
      -            self._service_tier = "priority"
      -            saved_value = "fast"
      -            label = t("gateway.fast.label_fast")
      -        elif args in {"normal", "off"}:
      -            self._service_tier = None
      -            saved_value = "normal"
      -            label = t("gateway.fast.label_normal")
      -        else:
      -            return t("gateway.fast.unknown_arg", arg=args)
      +            # Interactive picker on platforms that support it.
      +            session_key = self._session_key_for_source(event.source)
      +
      +            async def _on_fast_choice(_chat_id: str, value: str) -> str:
      +                return _apply_fast_selection(value)
      +
      +            picker_sent = await self._try_send_choice_picker(
      +                event,
      +                session_key,
      +                title=t("gateway.fast.picker_title", mode=status),
      +                choices=[
      +                    {
      +                        "value": "fast",
      +                        "label": t("gateway.fast.choice_fast"),
      +                        "is_current": is_fast,
      +                    },
      +                    {
      +                        "value": "normal",
      +                        "label": t("gateway.fast.choice_normal"),
      +                        "is_current": not is_fast,
      +                    },
      +                ],
      +                on_choice_selected=_on_fast_choice,
      +            )
      +            if picker_sent:
      +                return None  # Picker sent — adapter handles the response
      +
      +            return t("gateway.fast.status", mode=status)
       
      -        if _save_config_key("agent.service_tier", saved_value):
      -            return t("gateway.fast.saved", label=label)
      -        return t("gateway.fast.session_only", label=label)
      +        return _apply_fast_selection(args)
       
           async def _handle_yolo_command(self, event: MessageEvent) -> Union[str, EphemeralReply]:
               """Handle /yolo — toggle dangerous command approval bypass for this session only."""
    • locales/af.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nHuidige modus: `{mode}`\\n\\nKies \\'n opsie:"
      +    choice_fast:           "fast — Priority Processing aan"
      +    choice_normal:         "normal — standaardverwerking"
       
         footer:
           status:                "📎 Looptyd-voetstuk: **{state}**\nVelde: `{fields}`\nPlatform: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` word nie ondersteun nie. Gebruik `/reasoning <level> --global` om die globale verstek te verander."
           reset_done:                "🧠 ✓ Sessie-redenering-oorskryf verwyder; val terug op globale konfigurasie."
           unknown_arg:               "⚠️ Onbekende argument: `{arg}`\n\n**Geldige vlakke:** none, minimal, low, medium, high, xhigh, max, ultra\n**Vertoon:** show, hide\n**Permanent:** voeg `--global` by om verby hierdie sessie te stoor"
      +    picker_title:              "🧠 **Redenering-instellings**\\n\\n**Inspanning:** `{level}`\\n**Bereik:** {scope}\\n**Vertoon:** {display}\\n\\nKies \\'n opsie:"
      +    choice_none:               "none — deaktiveer redenering"
      +    choice_reset:              "reset — vee sessie-oorskrywing uit"
      +    choice_show:               "wys redenering in antwoorde"
      +    choice_hide:               "versteek redenering in antwoorde"
           set_global:                "🧠 ✓ Redenering-inspanning gestel op `{effort}` (gestoor in konfigurasie)\n_(neem effek by die volgende boodskap)_"
           set_global_save_failed:    "🧠 ✓ Redenering-inspanning gestel op `{effort}` (slegs sessie — konfigurasie-stoor het misluk)\n_(neem effek by die volgende boodskap)_"
           set_session:               "🧠 ✓ Redenering-inspanning gestel op `{effort}` (slegs sessie — voeg `--global` by om permanent te stoor)\n_(neem effek by die volgende boodskap)_"
    • locales/de.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nAktueller Modus: `{mode}`\\n\\nOption wählen:"
      +    choice_fast:           "fast — Priority Processing an"
      +    choice_normal:         "normal — Standardverarbeitung"
       
         footer:
           status:                "📎 Laufzeit-Fußzeile: **{state}**\nFelder: `{fields}`\nPlattform: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` wird nicht unterstützt. Verwenden Sie `/reasoning <level> --global`, um den globalen Standard zu ändern."
           reset_done:                "🧠 ✓ Sitzungs-Reasoning-Override gelöscht; Rückfall auf globale Konfiguration."
           unknown_arg:               "⚠️ Unbekanntes Argument: `{arg}`\n\n**Gültige Stärken:** none, minimal, low, medium, high, xhigh, max, ultra\n**Anzeige:** show, hide\n**Speichern:** `--global` hinzufügen, um über die Sitzung hinaus zu speichern"
      +    picker_title:              "🧠 **Reasoning-Einstellungen**\\n\\n**Stärke:** `{level}`\\n**Geltungsbereich:** {scope}\\n**Anzeige:** {display}\\n\\nOption wählen:"
      +    choice_none:               "none — Reasoning deaktivieren"
      +    choice_reset:              "reset — Sitzungs-Override löschen"
      +    choice_show:               "Reasoning in Antworten anzeigen"
      +    choice_hide:               "Reasoning in Antworten ausblenden"
           set_global:                "🧠 ✓ Reasoning-Stärke auf `{effort}` gesetzt (in Konfiguration gespeichert)\n_(wird mit der nächsten Nachricht wirksam)_"
           set_global_save_failed:    "🧠 ✓ Reasoning-Stärke auf `{effort}` gesetzt (nur Sitzung — Konfiguration konnte nicht gespeichert werden)\n_(wird mit der nächsten Nachricht wirksam)_"
           set_session:               "🧠 ✓ Reasoning-Stärke auf `{effort}` gesetzt (nur Sitzung — `--global` hinzufügen, um zu speichern)\n_(wird mit der nächsten Nachricht wirksam)_"
    • locales/en.yaml
      @@ -139,6 +139,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\n\nCurrent mode: `{mode}`\n\nPick an option:"
      +    choice_fast:           "fast — Priority Processing on"
      +    choice_normal:         "normal — standard processing"
       
         footer:
           status:                "📎 Runtime footer: **{state}**\nFields: `{fields}`\nPlatform: `{platform}`"
      @@ -209,6 +212,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` is not supported. Use `/reasoning <level> --global` to change the global default."
           reset_done:                "🧠 ✓ Session reasoning override cleared; falling back to global config."
           unknown_arg:               "⚠️ Unknown argument: `{arg}`\n\n**Valid levels:** none, minimal, low, medium, high, xhigh, max, ultra\n**Display:** show, hide\n**Persist:** add `--global` to save beyond this session"
      +    picker_title:              "🧠 **Reasoning Settings**\n\n**Effort:** `{level}`\n**Scope:** {scope}\n**Display:** {display}\n\nPick an option:"
      +    choice_none:               "none — disable reasoning"
      +    choice_reset:              "reset — clear session override"
      +    choice_show:               "show reasoning in replies"
      +    choice_hide:               "hide reasoning from replies"
           set_global:                "🧠 ✓ Reasoning effort set to `{effort}` (saved to config)\n_(takes effect on next message)_"
           set_global_save_failed:    "🧠 ✓ Reasoning effort set to `{effort}` (session only — config save failed)\n_(takes effect on next message)_"
           set_session:               "🧠 ✓ Reasoning effort set to `{effort}` (session only — add `--global` to persist)\n_(takes effect on next message)_"
    • locales/es.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nModo actual: `{mode}`\\n\\nElige una opción:"
      +    choice_fast:           "fast — Priority Processing activado"
      +    choice_normal:         "normal — procesamiento estándar"
       
         footer:
           status:                "📎 Pie de ejecución: **{state}**\nCampos: `{fields}`\nPlataforma: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` no es compatible. Usa `/reasoning <level> --global` para cambiar el valor global por defecto."
           reset_done:                "🧠 ✓ Anulación de razonamiento de la sesión borrada; volviendo a la configuración global."
           unknown_arg:               "⚠️ Argumento desconocido: `{arg}`\n\n**Niveles válidos:** none, minimal, low, medium, high, xhigh, max, ultra\n**Visualización:** show, hide\n**Persistir:** añade `--global` para guardar más allá de esta sesión"
      +    picker_title:              "🧠 **Ajustes de razonamiento**\\n\\n**Esfuerzo:** `{level}`\\n**Alcance:** {scope}\\n**Visualización:** {display}\\n\\nElige una opción:"
      +    choice_none:               "none — desactivar razonamiento"
      +    choice_reset:              "reset — borrar el ajuste de sesión"
      +    choice_show:               "mostrar razonamiento en respuestas"
      +    choice_hide:               "ocultar razonamiento en respuestas"
           set_global:                "🧠 ✓ Esfuerzo de razonamiento ajustado a `{effort}` (guardado en la configuración)\n_(se aplica en el próximo mensaje)_"
           set_global_save_failed:    "🧠 ✓ Esfuerzo de razonamiento ajustado a `{effort}` (solo en la sesión — error al guardar la configuración)\n_(se aplica en el próximo mensaje)_"
           set_session:               "🧠 ✓ Esfuerzo de razonamiento ajustado a `{effort}` (solo en la sesión — añade `--global` para persistir)\n_(se aplica en el próximo mensaje)_"
    • locales/fr.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nMode actuel : `{mode}`\\n\\nChoisissez une option :"
      +    choice_fast:           "fast — Priority Processing activé"
      +    choice_normal:         "normal — traitement standard"
       
         footer:
           status:                "📎 Pied de page d'exécution : **{state}**\nChamps : `{fields}`\nPlateforme : `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` n'est pas pris en charge. Utilisez `/reasoning <level> --global` pour modifier la valeur globale par défaut."
           reset_done:                "🧠 ✓ Remplacement de raisonnement de la session effacé ; retour à la configuration globale."
           unknown_arg:               "⚠️ Argument inconnu : `{arg}`\n\n**Niveaux valides :** none, minimal, low, medium, high, xhigh, max, ultra\n**Affichage :** show, hide\n**Persister :** ajoutez `--global` pour enregistrer au-delà de cette session"
      +    picker_title:              "🧠 **Paramètres de raisonnement**\\n\\n**Effort :** `{level}`\\n**Portée :** {scope}\\n**Affichage :** {display}\\n\\nChoisissez une option :"
      +    choice_none:               "none — désactiver le raisonnement"
      +    choice_reset:              "reset — effacer le réglage de session"
      +    choice_show:               "afficher le raisonnement dans les réponses"
      +    choice_hide:               "masquer le raisonnement dans les réponses"
           set_global:                "🧠 ✓ Effort de raisonnement défini sur `{effort}` (enregistré dans la configuration)\n_(prend effet au prochain message)_"
           set_global_save_failed:    "🧠 ✓ Effort de raisonnement défini sur `{effort}` (session uniquement — échec de l'enregistrement de la configuration)\n_(prend effet au prochain message)_"
           set_session:               "🧠 ✓ Effort de raisonnement défini sur `{effort}` (session uniquement — ajoutez `--global` pour persister)\n_(prend effet au prochain message)_"
    • locales/ga.yaml
      @@ -128,6 +128,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nMód reatha: `{mode}`\\n\\nRoghnaigh rogha:"
      +    choice_fast:           "fast — Priority Processing ar siúl"
      +    choice_normal:         "normal — gnáthphróiseáil"
       
         footer:
           status:                "📎 Buntásc rite: **{state}**\nRéimsí: `{fields}`\nArdán: `{platform}`"
      @@ -198,6 +201,11 @@ gateway:
           reset_global_unsupported:  "⚠️ Ní thacaítear le `/reasoning reset --global`. Úsáid `/reasoning <level> --global` chun an réamhshocrú domhanda a athrú."
           reset_done:                "🧠 ✓ Sárú réasúnaíochta seisiúin glanta; ag titim siar ar an gcumraíocht dhomhanda."
           unknown_arg:               "⚠️ Argóint anaithnid: `{arg}`\n\n**Leibhéil bhailí:** none, minimal, low, medium, high, xhigh, max, ultra\n**Taispeáint:** show, hide\n**Coinnigh:** cuir `--global` leis chun sábháil thar an seisiún seo"
      +    picker_title:              "🧠 **Socruithe Réasúnaíochta**\\n\\n**Iarracht:** `{level}`\\n**Scóip:** {scope}\\n**Taispeáint:** {display}\\n\\nRoghnaigh rogha:"
      +    choice_none:               "none — díchumasaigh réasúnaíocht"
      +    choice_reset:              "reset — glan sárú an tseisiúin"
      +    choice_show:               "taispeáin réasúnaíocht sna freagraí"
      +    choice_hide:               "folaigh réasúnaíocht ó na freagraí"
           set_global:                "🧠 ✓ Iarracht réasúnaíochta socraithe go `{effort}` (sábháilte sa chumraíocht)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
           set_global_save_failed:    "🧠 ✓ Iarracht réasúnaíochta socraithe go `{effort}` (seisiún amháin — theip ar shábháil cumraíochta)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
           set_session:               "🧠 ✓ Iarracht réasúnaíochta socraithe go `{effort}` (seisiún amháin — cuir `--global` leis chun é a choinneáil)\n_(éifeachtach ón gcéad teachtaireacht eile)_"
    • locales/hu.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nJelenlegi mód: `{mode}`\\n\\nVálassz egy opciót:"
      +    choice_fast:           "fast — Priority Processing bekapcsolva"
      +    choice_normal:         "normal — normál feldolgozás"
       
         footer:
           status:                "📎 Futási idejű lábléc: **{state}**\nMezők: `{fields}`\nPlatform: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ A `/reasoning reset --global` nem támogatott. Használd a `/reasoning <level> --global` parancsot a globális alapérték módosításához."
           reset_done:                "🧠 ✓ A munkamenet gondolkodási felülbírálása törölve; visszaállás a globális konfigurációra."
           unknown_arg:               "⚠️ Ismeretlen argumentum: `{arg}`\n\n**Érvényes szintek:** none, minimal, low, medium, high, xhigh, max, ultra\n**Megjelenítés:** show, hide\n**Megőrzés:** add hozzá a `--global` opciót a munkameneten túli mentéshez"
      +    picker_title:              "🧠 **Gondolkodási beállítások**\\n\\n**Erőfeszítés:** `{level}`\\n**Hatókör:** {scope}\\n**Megjelenítés:** {display}\\n\\nVálassz egy opciót:"
      +    choice_none:               "none — gondolkodás kikapcsolása"
      +    choice_reset:              "reset — munkameneti felülbírálás törlése"
      +    choice_show:               "gondolkodás megjelenítése a válaszokban"
      +    choice_hide:               "gondolkodás elrejtése a válaszokból"
           set_global:                "🧠 ✓ Gondolkodási erőfeszítés beállítva: `{effort}` (mentve a konfigurációba)\n_(a következő üzenettől lép életbe)_"
           set_global_save_failed:    "🧠 ✓ Gondolkodási erőfeszítés beállítva: `{effort}` (csak ebben a munkamenetben — a konfiguráció mentése sikertelen)\n_(a következő üzenettől lép életbe)_"
           set_session:               "🧠 ✓ Gondolkodási erőfeszítés beállítva: `{effort}` (csak ebben a munkamenetben — add hozzá a `--global` opciót a megőrzéshez)\n_(a következő üzenettől lép életbe)_"
    • locales/it.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nModalità attuale: `{mode}`\\n\\nScegli un\\'opzione:"
      +    choice_fast:           "fast — Priority Processing attivo"
      +    choice_normal:         "normal — elaborazione standard"
       
         footer:
           status:                "📎 Footer di runtime: **{state}**\nCampi: `{fields}`\nPiattaforma: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` non è supportato. Usa `/reasoning <level> --global` per cambiare il valore predefinito globale."
           reset_done:                "🧠 ✓ Override di reasoning della sessione cancellato; ripristino della configurazione globale."
           unknown_arg:               "⚠️ Argomento sconosciuto: `{arg}`\n\n**Livelli validi:** none, minimal, low, medium, high, xhigh, max, ultra\n**Visualizzazione:** show, hide\n**Persistenza:** aggiungi `--global` per salvare oltre questa sessione"
      +    picker_title:              "🧠 **Impostazioni di reasoning**\\n\\n**Sforzo:** `{level}`\\n**Ambito:** {scope}\\n**Visualizzazione:** {display}\\n\\nScegli un\\'opzione:"
      +    choice_none:               "none — disattiva il reasoning"
      +    choice_reset:              "reset — cancella l'override di sessione"
      +    choice_show:               "mostra il reasoning nelle risposte"
      +    choice_hide:               "nascondi il reasoning dalle risposte"
           set_global:                "🧠 ✓ Sforzo di reasoning impostato su `{effort}` (salvato nella configurazione)\n_(verrà applicato al prossimo messaggio)_"
           set_global_save_failed:    "🧠 ✓ Sforzo di reasoning impostato su `{effort}` (solo per questa sessione — salvataggio della configurazione non riuscito)\n_(verrà applicato al prossimo messaggio)_"
           set_session:               "🧠 ✓ Sforzo di reasoning impostato su `{effort}` (solo per questa sessione — aggiungi `--global` per renderlo permanente)\n_(verrà applicato al prossimo messaggio)_"
    • locales/ja.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\n現在のモード: `{mode}`\\n\\nオプションを選択:"
      +    choice_fast:           "fast — Priority Processing オン"
      +    choice_normal:         "normal — 標準処理"
       
         footer:
           status:                "📎 ランタイムフッター: **{state}**\nフィールド: `{fields}`\nプラットフォーム: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` はサポートされていません。グローバルのデフォルトを変更するには `/reasoning <level> --global` を使用してください。"
           reset_done:                "🧠 ✓ セッションの推論オーバーライドをクリアしました。グローバル設定にフォールバックします。"
           unknown_arg:               "⚠️ 不明な引数: `{arg}`\n\n**有効なレベル:** none, minimal, low, medium, high, xhigh, max, ultra\n**表示:** show, hide\n**永続化:** セッションを越えて保存するには `--global` を追加"
      +    picker_title:              "🧠 **推論設定**\\n\\n**強度:** `{level}`\\n**スコープ:** {scope}\\n**表示:** {display}\\n\\nオプションを選択:"
      +    choice_none:               "none — 推論を無効化"
      +    choice_reset:              "reset — セッション上書きをクリア"
      +    choice_show:               "返信に推論を表示"
      +    choice_hide:               "返信の推論を非表示"
           set_global:                "🧠 ✓ 推論強度を `{effort}` に設定しました (設定に保存)\n_(次のメッセージから有効)_"
           set_global_save_failed:    "🧠 ✓ 推論強度を `{effort}` に設定しました (セッションのみ — 設定の保存に失敗)\n_(次のメッセージから有効)_"
           set_session:               "🧠 ✓ 推論強度を `{effort}` に設定しました (セッションのみ — 永続化するには `--global` を追加)\n_(次のメッセージから有効)_"
    • locales/ko.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\n현재 모드: `{mode}`\\n\\n옵션을 선택하세요:"
      +    choice_fast:           "fast — Priority Processing 켜기"
      +    choice_normal:         "normal — 표준 처리"
       
         footer:
           status:                "📎 런타임 푸터: **{state}**\n필드: `{fields}`\n플랫폼: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global`은 지원되지 않습니다. 전역 기본값을 변경하려면 `/reasoning <level> --global`을 사용하세요."
           reset_done:                "🧠 ✓ 세션 추론 재정의가 해제되었습니다. 전역 설정으로 돌아갑니다."
           unknown_arg:               "⚠️ 알 수 없는 인수: `{arg}`\n\n**유효한 수준:** none, minimal, low, medium, high, xhigh, max, ultra\n**표시:** show, hide\n**영구화:** 이 세션을 넘어 저장하려면 `--global`을 추가하세요"
      +    picker_title:              "🧠 **추론 설정**\\n\\n**노력:** `{level}`\\n**범위:** {scope}\\n**표시:** {display}\\n\\n옵션을 선택하세요:"
      +    choice_none:               "none — 추론 비활성화"
      +    choice_reset:              "reset — 세션 재정의 초기화"
      +    choice_show:               "응답에 추론 표시"
      +    choice_hide:               "응답에서 추론 숨기기"
           set_global:                "🧠 ✓ 추론 노력이 `{effort}`(으)로 설정되었습니다 (설정에 저장됨)\n_(다음 메시지부터 적용됩니다)_"
           set_global_save_failed:    "🧠 ✓ 추론 노력이 `{effort}`(으)로 설정되었습니다 (세션 한정 — 설정 저장 실패)\n_(다음 메시지부터 적용됩니다)_"
           set_session:               "🧠 ✓ 추론 노력이 `{effort}`(으)로 설정되었습니다 (세션 한정 — 영구 저장하려면 `--global` 추가)\n_(다음 메시지부터 적용됩니다)_"
    • locales/pt.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nModo atual: `{mode}`\\n\\nEscolha uma opção:"
      +    choice_fast:           "fast — Priority Processing ativado"
      +    choice_normal:         "normal — processamento padrão"
       
         footer:
           status:                "📎 Rodapé de execução: **{state}**\nCampos: `{fields}`\nPlataforma: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` não é suportado. Usa `/reasoning <level> --global` para alterar o predefinido global."
           reset_done:                "🧠 ✓ Substituição de raciocínio da sessão removida; a regressar à configuração global."
           unknown_arg:               "⚠️ Argumento desconhecido: `{arg}`\n\n**Níveis válidos:** none, minimal, low, medium, high, xhigh, max, ultra\n**Visualização:** show, hide\n**Persistir:** adiciona `--global` para guardar para além desta sessão"
      +    picker_title:              "🧠 **Definições de raciocínio**\\n\\n**Esforço:** `{level}`\\n**Âmbito:** {scope}\\n**Visualização:** {display}\\n\\nEscolha uma opção:"
      +    choice_none:               "none — desativar raciocínio"
      +    choice_reset:              "reset — limpar o override de sessão"
      +    choice_show:               "mostrar raciocínio nas respostas"
      +    choice_hide:               "ocultar raciocínio das respostas"
           set_global:                "🧠 ✓ Esforço de raciocínio definido como `{effort}` (guardado na configuração)\n_(produz efeito na próxima mensagem)_"
           set_global_save_failed:    "🧠 ✓ Esforço de raciocínio definido como `{effort}` (apenas sessão — falha ao guardar a configuração)\n_(produz efeito na próxima mensagem)_"
           set_session:               "🧠 ✓ Esforço de raciocínio definido como `{effort}` (apenas sessão — adiciona `--global` para persistir)\n_(produz efeito na próxima mensagem)_"
    • locales/ru.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nТекущий режим: `{mode}`\\n\\nВыберите вариант:"
      +    choice_fast:           "fast — Priority Processing включён"
      +    choice_normal:         "normal — стандартная обработка"
       
         footer:
           status:                "📎 Нижний колонтитул среды выполнения: **{state}**\nПоля: `{fields}`\nПлатформа: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` не поддерживается. Используйте `/reasoning <level> --global`, чтобы изменить глобальное значение по умолчанию."
           reset_done:                "🧠 ✓ Переопределение рассуждений для сеанса сброшено; возврат к глобальной конфигурации."
           unknown_arg:               "⚠️ Неизвестный аргумент: `{arg}`\n\n**Допустимые уровни:** none, minimal, low, medium, high, xhigh, max, ultra\n**Отображение:** show, hide\n**Сохранение:** добавьте `--global`, чтобы сохранить за пределами этого сеанса"
      +    picker_title:              "🧠 **Настройки рассуждений**\\n\\n**Усилия:** `{level}`\\n**Область:** {scope}\\n**Отображение:** {display}\\n\\nВыберите вариант:"
      +    choice_none:               "none — отключить рассуждения"
      +    choice_reset:              "reset — сбросить переопределение сессии"
      +    choice_show:               "показывать рассуждения в ответах"
      +    choice_hide:               "скрывать рассуждения в ответах"
           set_global:                "🧠 ✓ Усилия рассуждений установлены на `{effort}` (сохранено в конфигурации)\n_(вступит в силу со следующего сообщения)_"
           set_global_save_failed:    "🧠 ✓ Усилия рассуждений установлены на `{effort}` (только этот сеанс — не удалось сохранить конфигурацию)\n_(вступит в силу со следующего сообщения)_"
           set_session:               "🧠 ✓ Усилия рассуждений установлены на `{effort}` (только этот сеанс — добавьте `--global`, чтобы сохранить)\n_(вступит в силу со следующего сообщения)_"
    • locales/tr.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nMevcut mod: `{mode}`\\n\\nBir seçenek seçin:"
      +    choice_fast:           "fast — Priority Processing açık"
      +    choice_normal:         "normal — standart işleme"
       
         footer:
           status:                "📎 Çalışma zamanı altbilgisi: **{state}**\nAlanlar: `{fields}`\nPlatform: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` desteklenmiyor. Genel varsayılanı değiştirmek için `/reasoning <level> --global` kullanın."
           reset_done:                "🧠 ✓ Oturumun akıl yürütme geçersiz kılması temizlendi; genel yapılandırmaya geri dönülüyor."
           unknown_arg:               "⚠️ Bilinmeyen argüman: `{arg}`\n\n**Geçerli seviyeler:** none, minimal, low, medium, high, xhigh, max, ultra\n**Görüntüleme:** show, hide\n**Kalıcı:** bu oturumun ötesinde kaydetmek için `--global` ekleyin"
      +    picker_title:              "🧠 **Akıl Yürütme Ayarları**\\n\\n**Güç:** `{level}`\\n**Kapsam:** {scope}\\n**Görüntüleme:** {display}\\n\\nBir seçenek seçin:"
      +    choice_none:               "none — akıl yürütmeyi kapat"
      +    choice_reset:              "reset — oturum geçersiz kılmasını temizle"
      +    choice_show:               "yanıtlarda akıl yürütmeyi göster"
      +    choice_hide:               "yanıtlarda akıl yürütmeyi gizle"
           set_global:                "🧠 ✓ Akıl yürütme gücü `{effort}` olarak ayarlandı (yapılandırmaya kaydedildi)\n_(sonraki mesajda etkili)_"
           set_global_save_failed:    "🧠 ✓ Akıl yürütme gücü `{effort}` olarak ayarlandı (yalnızca bu oturum — yapılandırma kaydedilemedi)\n_(sonraki mesajda etkili)_"
           set_session:               "🧠 ✓ Akıl yürütme gücü `{effort}` olarak ayarlandı (yalnızca bu oturum — kalıcı yapmak için `--global` ekleyin)\n_(sonraki mesajda etkili)_"
    • locales/uk.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\nПоточний режим: `{mode}`\\n\\nОберіть варіант:"
      +    choice_fast:           "fast — Priority Processing увімкнено"
      +    choice_normal:         "normal — стандартна обробка"
       
         footer:
           status:                "📎 Нижній колонтитул середовища: **{state}**\nПоля: `{fields}`\nПлатформа: `{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ `/reasoning reset --global` не підтримується. Використовуйте `/reasoning <level> --global`, щоб змінити глобальне значення за замовчуванням."
           reset_done:                "🧠 ✓ Перевизначення мислення для сеансу скинуто; повернення до глобальної конфігурації."
           unknown_arg:               "⚠️ Невідомий аргумент: `{arg}`\n\n**Дійсні рівні:** none, minimal, low, medium, high, xhigh, max, ultra\n**Показ:** show, hide\n**Зберегти:** додайте `--global`, щоб зберегти поза цим сеансом"
      +    picker_title:              "🧠 **Налаштування мислення**\\n\\n**Зусилля:** `{level}`\\n**Область:** {scope}\\n**Показ:** {display}\\n\\nОберіть варіант:"
      +    choice_none:               "none — вимкнути мислення"
      +    choice_reset:              "reset — скинути перевизначення сесії"
      +    choice_show:               "показувати мислення у відповідях"
      +    choice_hide:               "приховувати мислення у відповідях"
           set_global:                "🧠 ✓ Зусилля мислення встановлено на `{effort}` (збережено в конфігурації)\n_(набуде чинності з наступного повідомлення)_"
           set_global_save_failed:    "🧠 ✓ Зусилля мислення встановлено на `{effort}` (лише цей сеанс — не вдалося зберегти конфігурацію)\n_(набуде чинності з наступного повідомлення)_"
           set_session:               "🧠 ✓ Зусилля мислення встановлено на `{effort}` (лише цей сеанс — додайте `--global`, щоб зберегти)\n_(набуде чинності з наступного повідомлення)_"
    • locales/zh-hant.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **Priority Processing**\\n\\n目前模式:`{mode}`\\n\\n請選擇:"
      +    choice_fast:           "fast — 開啟 Priority Processing"
      +    choice_normal:         "normal — 標準處理"
       
         footer:
           status:                "📎 執行階段頁尾:**{state}**\n欄位:`{fields}`\n平台:`{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ 不支援 `/reasoning reset --global`。請使用 `/reasoning <level> --global` 變更全域預設值。"
           reset_done:                "🧠 ✓ 已清除本工作階段的推理覆寫;回退至全域設定。"
           unknown_arg:               "⚠️ 未知參數:`{arg}`\n\n**有效級別:** none, minimal, low, medium, high, xhigh, max, ultra\n**顯示:** show, hide\n**持久化:** 加上 `--global` 可跨工作階段儲存"
      +    picker_title:              "🧠 **推理設定**\\n\\n**強度:** `{level}`\\n**範圍:** {scope}\\n**顯示:** {display}\\n\\n請選擇:"
      +    choice_none:               "none — 停用推理"
      +    choice_reset:              "reset — 清除工作階段覆寫"
      +    choice_show:               "在回覆中顯示推理"
      +    choice_hide:               "在回覆中隱藏推理"
           set_global:                "🧠 ✓ 推理強度已設定為 `{effort}`(已儲存到設定)\n_(下一則訊息生效)_"
           set_global_save_failed:    "🧠 ✓ 推理強度已設定為 `{effort}`(僅本工作階段 — 設定儲存失敗)\n_(下一則訊息生效)_"
           set_session:               "🧠 ✓ 推理強度已設定為 `{effort}`(僅本工作階段 — 加上 `--global` 可持久化)\n_(下一則訊息生效)_"
    • locales/zh.yaml
      @@ -124,6 +124,9 @@ gateway:
           label_normal:          "NORMAL"
           status_fast:           "fast"
           status_normal:         "normal"
      +    picker_title:          "⚡ **优先处理**\\n\\n当前模式:`{mode}`\\n\\n请选择:"
      +    choice_fast:           "fast — 开启优先处理"
      +    choice_normal:         "normal — 标准处理"
       
         footer:
           status:                "📎 运行时页脚:**{state}**\n字段:`{fields}`\n平台:`{platform}`"
      @@ -194,6 +197,11 @@ gateway:
           reset_global_unsupported:  "⚠️ 不支持 `/reasoning reset --global`。请使用 `/reasoning <level> --global` 修改全局默认值。"
           reset_done:                "🧠 ✓ 已清除本会话的推理覆盖;回退到全局配置。"
           unknown_arg:               "⚠️ 未知参数:`{arg}`\n\n**有效级别:** none, minimal, low, medium, high, xhigh, max, ultra\n**显示:** show, hide\n**持久化:** 添加 `--global` 以跨会话保存"
      +    picker_title:              "🧠 **推理设置**\\n\\n**强度:** `{level}`\\n**作用域:** {scope}\\n**显示:** {display}\\n\\n请选择:"
      +    choice_none:               "none — 关闭推理"
      +    choice_reset:              "reset — 清除会话覆盖"
      +    choice_show:               "在回复中显示推理"
      +    choice_hide:               "在回复中隐藏推理"
           set_global:                "🧠 ✓ 推理强度已设置为 `{effort}`(已保存到配置)\n_(下一条消息生效)_"
           set_global_save_failed:    "🧠 ✓ 推理强度已设置为 `{effort}`(仅本会话 — 配置保存失败)\n_(下一条消息生效)_"
           set_session:               "🧠 ✓ 推理强度已设置为 `{effort}`(仅本会话 — 添加 `--global` 以持久化)\n_(下一条消息生效)_"
    • plugins/platforms/discord/adapter.py
      @@ -5961,6 +5961,54 @@ async def send_model_picker(
                   logger.warning("[%s] send_model_picker failed: %s", self.name, e)
                   return SendResult(success=False, error=str(e))
       
      +    async def send_choice_picker(
      +        self,
      +        chat_id: str,
      +        title: str,
      +        choices: list,
      +        session_key: str,
      +        on_choice_selected,
      +        metadata: Optional[Dict[str, Any]] = None,
      +    ) -> SendResult:
      +        """Send a flat select-menu choice picker (one selection → one value).
      +
      +        Generic single-level companion to ``send_model_picker`` used by
      +        `/reasoning`, `/fast`, and any future finite-choice command. Each
      +        choice dict: ``{"value": str, "label": str, "is_current": bool}``.
      +        """
      +        if not self._client or not DISCORD_AVAILABLE:
      +            return SendResult(success=False, error="Not connected")
      +
      +        try:
      +            target_id = chat_id
      +            if metadata and metadata.get("thread_id"):
      +                target_id = metadata["thread_id"]
      +
      +            channel = self._client.get_channel(int(target_id))
      +            if not channel:
      +                channel = await self._client.fetch_channel(int(target_id))
      +
      +            embed = discord.Embed(
      +                title="⚙ " + (title.splitlines()[0] if title else "Choose an option"),
      +                description="\n".join(title.splitlines()[1:]) or None,
      +                color=discord.Color.blue(),
      +            )
      +
      +            view = ChoicePickerView(
      +                choices=choices,
      +                on_choice_selected=on_choice_selected,
      +                allowed_user_ids=self._allowed_user_ids,
      +                allowed_role_ids=self._allowed_role_ids,
      +            )
      +
      +            msg = await channel.send(embed=embed, view=view)
      +            view._message = msg  # store for on_timeout expiration editing
      +            return SendResult(success=True, message_id=str(msg.id))
      +
      +        except Exception as e:
      +            logger.warning("[%s] send_choice_picker failed: %s", self.name, e)
      +            return SendResult(success=False, error=str(e))
      +
           def _get_parent_channel_id(self, channel: Any) -> Optional[str]:
               """Return the parent channel ID for a Discord thread-like channel, if present."""
               parent = getattr(channel, "parent", None)
      @@ -6850,7 +6898,7 @@ def _define_discord_view_classes() -> None:
           lazy install sets DISCORD_AVAILABLE=True but leaves the classes
           undefined, causing NameError on the first button interaction.
           """
      -    global ExecApprovalView, SlashConfirmView, UpdatePromptView, ModelPickerView, ClarifyChoiceView
      +    global ExecApprovalView, SlashConfirmView, UpdatePromptView, ModelPickerView, ClarifyChoiceView, ChoicePickerView
       
           class ExecApprovalView(discord.ui.View):
               """
      @@ -7547,6 +7595,97 @@ async def on_timeout(self):
                           pass
       
       
      +    class ChoicePickerView(discord.ui.View):
      +        """Flat select-menu view for finite-choice commands (/reasoning, /fast).
      +
      +        One dropdown, one selection, done — the generic single-level companion
      +        to ``ModelPickerView``. Auth gating mirrors ``ExecApprovalView``.
      +        Times out after 2 minutes.
      +        """
      +
      +        def __init__(
      +            self,
      +            choices: list,
      +            on_choice_selected,
      +            allowed_user_ids: set,
      +            allowed_role_ids: Optional[set] = None,
      +        ):
      +            super().__init__(timeout=120)
      +            self.choices = list(choices)[:25]  # Discord select cap
      +            self.on_choice_selected = on_choice_selected
      +            self.allowed_user_ids = allowed_user_ids
      +            self.allowed_role_ids = allowed_role_ids or set()
      +            self.resolved = False
      +            self._message = None
      +
      +            options = []
      +            for choice in self.choices:
      +                label = str(choice.get("label") or choice.get("value") or "")
      +                options.append(
      +                    discord.SelectOption(
      +                        label=_truncate_discord_component_text(
      +                            label, _DISCORD_SELECT_FIELD_LIMIT
      +                        ),
      +                        value=str(choice.get("value") or ""),
      +                        description="current" if choice.get("is_current") else None,
      +                    )
      +                )
      +            select = discord.ui.Select(
      +                placeholder="Choose an option...",
      +                options=options,
      +            )
      +            select.callback = self._on_select
      +            self.add_item(select)
      +
      +        def _check_auth(self, interaction: discord.Interaction) -> bool:
      +            return _component_check_auth(
      +                interaction, self.allowed_user_ids, self.allowed_role_ids,
      +            )
      +
      +        async def _on_select(self, interaction: discord.Interaction):
      +            if not self._check_auth(interaction):
      +                await interaction.response.send_message(
      +                    "⛔ You are not authorized to change this setting.",
      +                    ephemeral=True,
      +                )
      +                return
      +            if self.resolved:
      +                await interaction.response.defer()
      +                return
      +            self.resolved = True
      +
      +            value = interaction.data.get("values", [""])[0]
      +            try:
      +                result_text = await self.on_choice_selected(
      +                    str(interaction.channel_id), value
      +                )
      +            except Exception as exc:
      +                logger.error("Choice picker selection failed: %s", exc)
      +                result_text = f"Error applying selection: {exc}"
      +
      +            embed = discord.Embed(
      +                description=result_text,
      +                color=discord.Color.green(),
      +            )
      +            self.clear_items()
      +            self.stop()
      +            await interaction.response.edit_message(embed=embed, view=self)
      +
      +        async def on_timeout(self):
      +            if self.resolved:
      +                return
      +            msg = self._message
      +            if msg is not None:
      +                try:
      +                    embed = discord.Embed(
      +                        description="⏱ Selection expired — no change made.",
      +                        color=discord.Color.greyple(),
      +                    )
      +                    self.clear_items()
      +                    await msg.edit(embed=embed, view=self)
      +                except Exception:
      +                    pass
      +
           class ClarifyChoiceView(discord.ui.View):
               """Interactive button view for the clarify tool's multiple-choice prompts.
       
    • plugins/platforms/matrix/adapter.py
      @@ -337,6 +337,21 @@ class _MatrixModelPickerPrompt:
           bot_reaction_events: dict[str, str] = field(default_factory=dict)
       
       
      +@dataclass
      +class _MatrixChoicePickerPrompt:
      +    """Tracks a pending Matrix reaction-based choice picker (/reasoning, /fast)."""
      +
      +    chat_id: str
      +    message_id: str
      +    session_key: str
      +    choices: dict[str, str]  # emoji -> value
      +    on_choice_selected: Any
      +    requester_user_id: str | None = None
      +    expires_at: float | None = None
      +    resolved: bool = False
      +    bot_reaction_events: dict[str, str] = field(default_factory=dict)
      +
      +
       # Matrix message size limit (4000 chars practical, spec has no hard limit
       # but clients render poorly above this).
       MAX_MESSAGE_LENGTH = 4000
      @@ -386,6 +401,14 @@ class _MatrixModelPickerPrompt:
           "\U0001f51f",
       )
       
      +# Choice pickers (/reasoning, /fast) can need more than 10 slots
      +# (8 effort levels + none + reset/show/hide = 12), so extend the keycap
      +# set with lettered squares.
      +_MATRIX_CHOICE_PICKER_REACTIONS = _MATRIX_MODEL_PICKER_REACTIONS + (
      +    "\U0001f170\ufe0f",  # 🅰️
      +    "\U0001f171\ufe0f",  # 🅱️
      +)
      +
       _MATRIX_CAPABILITIES: Dict[str, str] = {
           "text": "yes",
           "threads": "yes",
      @@ -954,6 +977,7 @@ def __init__(self, config: PlatformConfig):
               except ValueError:
                   self._approval_timeout_seconds = 300
               self._model_picker_prompts_by_event: Dict[str, _MatrixModelPickerPrompt] = {}
      +        self._choice_picker_prompts_by_event: Dict[str, _MatrixChoicePickerPrompt] = {}
               allowed_users_raw = os.getenv("MATRIX_ALLOWED_USERS", "")
               self._allowed_user_ids: Set[str] = {
                   u.strip() for u in allowed_users_raw.split(",") if u.strip()
      @@ -2136,6 +2160,66 @@ async def send_model_picker(
       
               return result
       
      +    async def send_choice_picker(
      +        self,
      +        chat_id: str,
      +        title: str,
      +        choices: list,
      +        session_key: str,
      +        on_choice_selected,
      +        metadata: Optional[Dict[str, Any]] = None,
      +    ) -> SendResult:
      +        """Send a Matrix reaction-based choice picker (/reasoning, /fast).
      +
      +        Generic single-level companion to ``send_model_picker``. Each choice
      +        dict: ``{"value": str, "label": str, "is_current": bool}``.
      +        """
      +        if not self._client:
      +            return SendResult(success=False, error="Not connected")
      +
      +        emoji_choices: dict[str, str] = {}
      +        lines = [title, ""]
      +        for i, choice in enumerate(choices):
      +            if i >= len(_MATRIX_CHOICE_PICKER_REACTIONS):
      +                break
      +            emoji = _MATRIX_CHOICE_PICKER_REACTIONS[i]
      +            value = str(choice.get("value") or "")
      +            label = str(choice.get("label") or value)
      +            if choice.get("is_current"):
      +                label = f"{label} ← current"
      +            emoji_choices[emoji] = value
      +            lines.append(f"{emoji} {label}")
      +
      +        if not emoji_choices:
      +            return SendResult(success=False, error="No choices")
      +
      +        lines.append("")
      +        lines.append("React to choose.")
      +        result = await self.send(chat_id, "\n".join(lines), metadata=metadata)
      +        if not result.success or not result.message_id:
      +            return result
      +
      +        prompt = _MatrixChoicePickerPrompt(
      +            chat_id=chat_id,
      +            message_id=result.message_id,
      +            session_key=session_key,
      +            choices=emoji_choices,
      +            on_choice_selected=on_choice_selected,
      +            requester_user_id=str((metadata or {}).get("requester_user_id") or "") or None,
      +            expires_at=time.monotonic() + max(self._approval_timeout_seconds, 0),
      +        )
      +        self._choice_picker_prompts_by_event[result.message_id] = prompt
      +
      +        for emoji in emoji_choices:
      +            try:
      +                reaction_event_id = await self._send_reaction(chat_id, result.message_id, emoji)
      +                if reaction_event_id:
      +                    prompt.bot_reaction_events[emoji] = str(reaction_event_id)
      +            except Exception as exc:
      +                logger.debug("Matrix: failed to add choice picker reaction %s: %s", emoji, exc)
      +
      +        return result
      +
           def format_message(self, content: str) -> str:
               """Pass-through — Matrix supports standard Markdown natively."""
               # Strip image markdown; media is uploaded separately.
      @@ -3328,6 +3412,40 @@ async def _on_reaction(self, event: Any) -> None:
                           )
                       return
       
      +            choice_prompt = self._choice_picker_prompts_by_event.get(reacts_to)
      +            if choice_prompt and not choice_prompt.resolved:
      +                if room_id != choice_prompt.chat_id:
      +                    return
      +                if self._matrix_prompt_expired(choice_prompt):
      +                    self._choice_picker_prompts_by_event.pop(reacts_to, None)
      +                    return
      +                if not await self._validate_matrix_prompt_reactor(
      +                    room_id, reacts_to, sender, choice_prompt, "choice picker"
      +                ):
      +                    return
      +                value = choice_prompt.choices.get(key)
      +                if value is None:
      +                    await self._send_invalid_reaction_feedback(
      +                        room_id,
      +                        reacts_to,
      +                        "That reaction is not one of the available choices.",
      +                    )
      +                    return
      +                choice_prompt.resolved = True
      +                self._choice_picker_prompts_by_event.pop(reacts_to, None)
      +                try:
      +                    confirmation = await choice_prompt.on_choice_selected(room_id, value)
      +                    if confirmation:
      +                        await self.send(room_id, confirmation, reply_to=reacts_to)
      +                except Exception as exc:
      +                    logger.error("Failed to apply choice from Matrix reaction: %s", exc)
      +                    await self.send(
      +                        room_id,
      +                        f"Failed to apply selection: {exc}",
      +                        reply_to=reacts_to,
      +                    )
      +                return
      +
           def _matrix_prompt_expired(self, prompt: Any) -> bool:
               expires_at = getattr(prompt, "expires_at", None)
               return expires_at is not None and time.monotonic() > float(expires_at)
    • plugins/platforms/telegram/adapter.py
      @@ -778,6 +778,7 @@ def __init__(self, config: PlatformConfig):
               )
               # Interactive model picker state per chat
               self._model_picker_state: Dict[str, dict] = {}
      +        self._choice_picker_state: Dict[str, dict] = {}
               # Approval button state: message_id → session_key
               self._approval_state: Dict[int, str] = {}
               # Slash-confirm button state: confirm_id → session_key (for /reload-mcp
      @@ -5213,6 +5214,128 @@ def get_label(slug):
                   return SendResult(success=False, error=_redact_telegram_error_text(e))
       
           _PROVIDER_PAGE_SIZE = 10
      +
      +    async def send_choice_picker(
      +        self,
      +        chat_id: str,
      +        title: str,
      +        choices: list,
      +        session_key: str,
      +        on_choice_selected,
      +        metadata: Optional[Dict[str, Any]] = None,
      +    ) -> SendResult:
      +        """Send a flat inline-keyboard choice picker (one tap → one value).
      +
      +        Generic single-level companion to ``send_model_picker`` used by
      +        `/reasoning`, `/fast`, and any future finite-choice command. Each
      +        choice dict: ``{"value": str, "label": str, "is_current": bool}``.
      +        """
      +        if not self._bot:
      +            return SendResult(success=False, error="Not connected")
      +
      +        try:
      +            buttons = []
      +            for i, choice in enumerate(choices):
      +                label = str(choice.get("label") or choice.get("value") or "")
      +                if choice.get("is_current"):
      +                    label = f"✓ {label}"
      +                buttons.append(
      +                    InlineKeyboardButton(label, callback_data=f"cp:{i}")
      +                )
      +            if not buttons:
      +                return SendResult(success=False, error="No choices")
      +            # Two buttons per row keeps labels readable on mobile.
      +            keyboard = InlineKeyboardMarkup(
      +                [buttons[i:i + 2] for i in range(0, len(buttons), 2)]
      +            )
      +
      +            thread_id = metadata.get("thread_id") if metadata else None
      +            reply_to_id = self._reply_to_message_id_for_send(None, metadata, reply_to_mode=self._reply_to_mode)
      +            msg = await self._send_message_with_thread_fallback(
      +                chat_id=normalize_telegram_chat_id(chat_id),
      +                text=self.format_message(title),
      +                parse_mode=ParseMode.MARKDOWN_V2,
      +                reply_markup=keyboard,
      +                reply_to_message_id=reply_to_id,
      +                **self._thread_kwargs_for_send(
      +                    chat_id,
      +                    thread_id,
      +                    metadata,
      +                    reply_to_message_id=reply_to_id,
      +                    reply_to_mode=self._reply_to_mode
      +                ),
      +                **self._link_preview_kwargs(),
      +            )
      +
      +            self._choice_picker_state[str(chat_id)] = {
      +                "msg_id": msg.message_id,
      +                "choices": choices,
      +                "session_key": session_key,
      +                "on_choice_selected": on_choice_selected,
      +            }
      +            return SendResult(success=True, message_id=str(msg.message_id))
      +        except Exception as e:
      +            logger.warning("[%s] send_choice_picker failed: %s", self.name, _redact_telegram_error_text(e))
      +            return SendResult(success=False, error=_redact_telegram_error_text(e))
      +
      +    async def _handle_choice_picker_callback(
      +        self, query, data: str, chat_id: str
      +    ) -> None:
      +        """Handle choice picker button taps (cp:<index>)."""
      +        state = self._choice_picker_state.get(chat_id)
      +        if not state:
      +            await query.answer(text="Picker expired — run the command again.")
      +            return
      +
      +        # Same authorization gate as approval buttons: unauthorized users in a
      +        # shared group must not flip session/config state via someone else's
      +        # picker message.
      +        query_message = getattr(query, "message", None)
      +        query_chat = getattr(query_message, "chat", None)
      +        if not self._is_callback_user_authorized(
      +            str(getattr(query.from_user, "id", "")),
      +            chat_id=getattr(query_message, "chat_id", None),
      +            chat_type=str(getattr(query_chat, "type", None)) if getattr(query_chat, "type", None) is not None else None,
      +            thread_id=str(getattr(query_message, "message_thread_id", None)) if getattr(query_message, "message_thread_id", None) is not None else None,
      +            user_name=getattr(query.from_user, "first_name", None),
      +        ):
      +            await query.answer(text="⛔ You are not authorized to change this setting.")
      +            return
      +
      +        try:
      +            idx = int(data[3:])
      +            choice = state["choices"][idx]
      +        except (ValueError, IndexError):
      +            await query.answer(text="Invalid selection.")
      +            return
      +
      +        callback = state.get("on_choice_selected")
      +        if not callback:
      +            await query.answer(text="Picker expired.")
      +            return
      +
      +        try:
      +            result_text = await callback(chat_id, str(choice.get("value") or ""))
      +        except Exception as exc:
      +            logger.error("Choice picker selection failed: %s", exc)
      +            result_text = f"Error applying selection: {exc}"
      +
      +        try:
      +            await query.edit_message_text(
      +                text=self.format_message(result_text),
      +                parse_mode=ParseMode.MARKDOWN_V2,
      +                reply_markup=None,
      +            )
      +        except Exception:
      +            try:
      +                await query.edit_message_text(
      +                    text=result_text, parse_mode=None, reply_markup=None,
      +                )
      +            except Exception:
      +                pass
      +        await query.answer()
      +        self._choice_picker_state.pop(chat_id, None)
      +
           _MODEL_PAGE_SIZE = 8
       
           def _build_provider_keyboard(self, providers: list, page: int = 0) -> tuple:
      @@ -5709,6 +5832,13 @@ async def _handle_callback_query(
                       await self._handle_model_picker_callback(query, data, chat_id)
                   return
       
      +        # --- Generic choice picker callbacks (/reasoning, /fast) ---
      +        if data.startswith("cp:"):
      +            chat_id = str(query.message.chat_id) if query.message else None
      +            if chat_id:
      +                await self._handle_choice_picker_callback(query, data, chat_id)
      +            return
      +
               # --- Gmail-triage callbacks (gt:verb:arg) ---
               if data.startswith("gt:"):
                   await self._handle_gmail_triage_callback(
    • tests/gateway/test_choice_picker.py
      @@ -0,0 +1,224 @@
      +"""Tests for the gateway interactive choice picker (/reasoning, /fast).
      +
      +The picker mirrors the /model picker architecture: the gateway gates on the
      +adapter *type* exposing ``send_choice_picker``, sends a flat choice list, and
      +falls back to the text status card when the platform has no picker or the
      +send fails. Selection flows through the same application path as the typed
      +command, so picker and typed arguments can never diverge.
      +"""
      +
      +import asyncio
      +from unittest.mock import AsyncMock, MagicMock
      +
      +import pytest
      +import yaml
      +
      +import gateway.run as gateway_run
      +from gateway.config import Platform
      +from gateway.platforms.base import MessageEvent, SendResult
      +from gateway.session import SessionSource
      +
      +
      +def _make_event(text="/reasoning", platform=Platform.TELEGRAM, user_id="12345", chat_id="67890"):
      +    source = SessionSource(
      +        platform=platform,
      +        user_id=user_id,
      +        chat_id=chat_id,
      +        user_name="testuser",
      +    )
      +    return MessageEvent(text=text, source=source)
      +
      +
      +class _PickerAdapter:
      +    """Adapter whose *type* exposes ``send_choice_picker`` (the gate the
      +    handler checks via ``getattr(type(adapter), 'send_choice_picker', None)``)."""
      +
      +    def __init__(self, success=True):
      +        self.calls = []
      +        self._success = success
      +
      +    async def send_choice_picker(self, **kwargs):
      +        self.calls.append(kwargs)
      +        return SendResult(success=self._success, message_id="m1")
      +
      +
      +class _NoPickerAdapter:
      +    """Adapter with no choice-picker capability."""
      +
      +
      +def _make_runner(adapter=None):
      +    runner = object.__new__(gateway_run.GatewayRunner)
      +    runner.adapters = {}
      +    runner._ephemeral_system_prompt = ""
      +    runner._prefill_messages = []
      +    runner._reasoning_config = None
      +    runner._session_reasoning_overrides = {}
      +    runner._show_reasoning = False
      +    runner._provider_routing = {}
      +    runner._fallback_model = None
      +    runner._running_agents = {}
      +    runner.hooks = MagicMock()
      +    runner.hooks.emit = AsyncMock()
      +    runner.hooks.loaded_hooks = []
      +    runner._session_db = None
      +    runner._get_or_create_gateway_honcho = lambda session_key: (None, None)
      +    runner._adapter_for_source = lambda source: adapter
      +    runner._thread_metadata_for_source = lambda source, anchor=None: {}
      +    runner._reply_anchor_for_event = lambda event: None
      +    return runner
      +
      +
      +class TestReasoningChoicePicker:
      +    @pytest.mark.asyncio
      +    async def test_bare_reasoning_sends_picker_when_adapter_supports_it(self, tmp_path, monkeypatch):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +
      +        result = await runner._handle_reasoning_command(_make_event("/reasoning"))
      +
      +        assert result is None  # picker sent — adapter owns the response
      +        assert len(adapter.calls) == 1
      +        call = adapter.calls[0]
      +        values = [c["value"] for c in call["choices"]]
      +        # Full canonical ladder + none + subcommands, in order
      +        from hermes_constants import VALID_REASONING_EFFORTS
      +        assert values[0] == "none"
      +        assert values[1:1 + len(VALID_REASONING_EFFORTS)] == list(VALID_REASONING_EFFORTS)
      +        assert values[-3:] == ["reset", "show", "hide"]
      +
      +    @pytest.mark.asyncio
      +    async def test_bare_reasoning_falls_back_to_text_without_picker(self, tmp_path, monkeypatch):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        runner = _make_runner(_NoPickerAdapter())
      +
      +        result = await runner._handle_reasoning_command(_make_event("/reasoning"))
      +
      +        assert isinstance(result, str)
      +        assert "/reasoning" in result  # text status card
      +
      +    @pytest.mark.asyncio
      +    async def test_bare_reasoning_falls_back_to_text_when_picker_send_fails(self, tmp_path, monkeypatch):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        adapter = _PickerAdapter(success=False)
      +        runner = _make_runner(adapter)
      +
      +        result = await runner._handle_reasoning_command(_make_event("/reasoning"))
      +
      +        assert isinstance(result, str)
      +        assert len(adapter.calls) == 1  # attempted, then fell back
      +
      +    @pytest.mark.asyncio
      +    async def test_typed_argument_never_sends_picker(self, tmp_path, monkeypatch):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +
      +        result = await runner._handle_reasoning_command(_make_event("/reasoning high"))
      +
      +        assert isinstance(result, str)
      +        assert adapter.calls == []
      +
      +    @pytest.mark.asyncio
      +    async def test_picker_selection_applies_same_as_typed(self, tmp_path, monkeypatch):
      +        """The picker's on_choice_selected must produce the identical state
      +        change as typing the argument (single application path)."""
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +        event = _make_event("/reasoning")
      +        session_key = runner._session_key_for_source(event.source)
      +
      +        await runner._handle_reasoning_command(event)
      +        on_choice = adapter.calls[0]["on_choice_selected"]
      +
      +        reply = await on_choice(event.source.chat_id, "ultra")
      +
      +        assert "ultra" in reply
      +        override = runner._session_reasoning_overrides.get(session_key)
      +        assert override == {"enabled": True, "effort": "ultra"}
      +
      +    @pytest.mark.asyncio
      +    async def test_picker_selection_of_current_level_marks_is_current(self, tmp_path, monkeypatch):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        (tmp_path / "config.yaml").write_text(
      +            yaml.safe_dump({"agent": {"reasoning_effort": "xhigh"}}), encoding="utf-8"
      +        )
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +
      +        await runner._handle_reasoning_command(_make_event("/reasoning"))
      +
      +        current = [c["value"] for c in adapter.calls[0]["choices"] if c.get("is_current")]
      +        assert current == ["xhigh"]
      +
      +    @pytest.mark.asyncio
      +    async def test_picker_show_choice_toggles_display(self, tmp_path, monkeypatch):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +        event = _make_event("/reasoning")
      +
      +        await runner._handle_reasoning_command(event)
      +        on_choice = adapter.calls[0]["on_choice_selected"]
      +        await on_choice(event.source.chat_id, "show")
      +
      +        assert runner._show_reasoning is True
      +        saved = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
      +        assert saved["display"]["platforms"]["telegram"]["show_reasoning"] is True
      +
      +
      +class TestFastChoicePicker:
      +    def _patch_fast_support(self, monkeypatch, tmp_path):
      +        monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
      +        monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
      +        monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda cfg: "gpt-5.6")
      +        import hermes_cli.models as models_mod
      +        monkeypatch.setattr(models_mod, "model_supports_fast_mode", lambda m: True)
      +
      +    @pytest.mark.asyncio
      +    async def test_bare_fast_sends_picker_when_adapter_supports_it(self, tmp_path, monkeypatch):
      +        self._patch_fast_support(monkeypatch, tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +
      +        result = await runner._handle_fast_command(_make_event("/fast"))
      +
      +        assert result is None
      +        values = [c["value"] for c in adapter.calls[0]["choices"]]
      +        assert values == ["fast", "normal"]
      +
      +    @pytest.mark.asyncio
      +    async def test_fast_picker_selection_persists_service_tier(self, tmp_path, monkeypatch):
      +        self._patch_fast_support(monkeypatch, tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +        event = _make_event("/fast")
      +
      +        await runner._handle_fast_command(event)
      +        on_choice = adapter.calls[0]["on_choice_selected"]
      +        await on_choice(event.source.chat_id, "fast")
      +
      +        assert runner._service_tier == "priority"
      +        saved = yaml.safe_load((tmp_path / "config.yaml").read_text(encoding="utf-8"))
      +        assert saved["agent"]["service_tier"] == "fast"
      +
      +    @pytest.mark.asyncio
      +    async def test_bare_fast_falls_back_to_text_without_picker(self, tmp_path, monkeypatch):
      +        self._patch_fast_support(monkeypatch, tmp_path)
      +        runner = _make_runner(_NoPickerAdapter())
      +
      +        result = await runner._handle_fast_command(_make_event("/fast"))
      +
      +        assert isinstance(result, str)
      +
      +    @pytest.mark.asyncio
      +    async def test_typed_fast_argument_never_sends_picker(self, tmp_path, monkeypatch):
      +        self._patch_fast_support(monkeypatch, tmp_path)
      +        adapter = _PickerAdapter()
      +        runner = _make_runner(adapter)
      +
      +        result = await runner._handle_fast_command(_make_event("/fast normal"))
      +
      +        assert isinstance(result, str)
      +        assert adapter.calls == []
Closed issues · 1
  • Closed#61110Feature: Telegram inline pickers for /reasoning and /fast (parity with /model)
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.39: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#65799
Closed issues
1
Attempts
0 PRs closed without merging
Fix link checked
16 Jul 2026

For your agent

The same facts, typed.