From 6bac0e460a025e00b2a231085c96334ed0e498a1 Mon Sep 17 00:00:00 2001 From: Iko Date: Sun, 13 Sep 2026 06:33:45 -0700 Subject: [PATCH] feat(builder): render-mode-aware tool output (plain text in TUI, JSON in CLI) --- __init__.py | 20 ++++++++++++++ _format.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/__init__.py b/__init__.py index 46e878a..58f4628 100644 --- a/__init__.py +++ b/__init__.py @@ -270,6 +270,7 @@ def _handle_q_debug(args: dict[str, Any], **kwargs: Any) -> str: "phase": status.get("phase"), "token_expires_at": status.get("token_expires_at"), "refreshed": status.get("refreshed"), + "token_expires_at_iso": status.get("token_expires_at_iso"), }, "identity": { "token_type": identity.get("token_type"), @@ -284,6 +285,24 @@ def _handle_q_debug(args: dict[str, Any], **kwargs: Any) -> str: return _success(payload) +def _plugin_transform_tool_result( + tool_name: str, + args: dict, + result: str, + **kwargs: Any, +) -> str | None: + """Transform tool result for TUI display only. + + Preserves the JSON envelope for structured callers (verify.py, scripts). + Only replaces the display string when render_mode is explicitly "tui". + """ + try: + from . import _format # package import + except ImportError: + import _format # type: ignore + return _format._plugin_transform_tool_result(tool_name, args, result, **kwargs) + + # --- tool registry --- _TOOLS = ( @@ -422,6 +441,7 @@ def register(ctx) -> None: # Register hook AFTER successful tool registration so a partial failure # doesn't leave an orphaned hook with _registered=False. ctx.register_hook("pre_tool_call", _plugin_pre_tool_call) + ctx.register_hook("transform_tool_result", _plugin_transform_tool_result) _registered = True # Best-effort: start the local OpenAI-compatible adapter so Hermes can diff --git a/_format.py b/_format.py index 5cf7881..25fdd83 100644 --- a/_format.py +++ b/_format.py @@ -16,6 +16,7 @@ from __future__ import annotations from collections.abc import Callable +from typing import Any _cache: dict[str, str] | None = None _load_config_fn: Callable[[], dict] | None = None @@ -58,3 +59,78 @@ def reset_prefs_cache() -> None: """Drop the cached prefs (tests).""" global _cache _cache = None + + +def _plugin_transform_tool_result( + tool_name: str, + args: dict, + result: str, + **kwargs: Any, +) -> str | None: + """Transform tool result for TUI display only. + + Preserves the JSON envelope for structured callers (verify.py, scripts). + Only replaces the display string when render_mode is explicitly "tui". + """ + try: + prefs = load_render_prefs() + except (KeyError, TypeError): + return None + + render_mode = prefs.get("render_mode", "auto") + if render_mode != "tui": + return None + + try: + import json + + payload = json.loads(result) + except (json.JSONDecodeError, TypeError): + return None + + if tool_name == "models": + models = payload.get("models", []) + tags = payload.get("tags", []) + lines = ["Available models:"] + for m in models: + lines.append(f" • {m}") + if tags: + lines.append("Tags:") + for t in tags: + lines.append(f" • {t}") + return "\n".join(lines) + + if tool_name == "tags": + tags = payload.get("tags", []) + return "Tags:\n" + "\n".join(f" • {t}" for t in tags) + + if tool_name == "q_debug": + return _format_q_debug_tui(payload) + + return None + + +def _format_q_debug_tui(payload: dict) -> str: + auth = payload.get("auth", {}) + identity = payload.get("identity", {}) + models = payload.get("models", []) + tags = payload.get("tags", []) + render = payload.get("render", []) + + lines = ["Builder ID Status"] + lines.append( + f" Auth: {'authenticated' if auth.get('authenticated') else 'not authenticated'} {auth.get('phase', 'unknown')}" + ) + if auth.get("token_expires_at_iso"): + lines.append(f" Expires: {auth['token_expires_at_iso']}") + if identity.get("token_type"): + lines.append(f" Token: {identity['token_type']}") + if identity.get("has_refresh_token") is not None: + lines.append(f" Refresh: {'yes' if identity['has_refresh_token'] else 'no'}") + if identity.get("scopes"): + lines.append(f" Scopes: {', '.join(identity['scopes'])}") + lines.append(f" Models: {', '.join(models)}") + lines.append(f" Tags: {', '.join(tags)}") + if render: + lines.append(f" Render: {render}") + return "\n".join(lines)