defmodule Mix.Interactive.State do @moduledoc """ State inspection utilities for the interactive MCP shell. This module provides functions for inspecting, formatting, and displaying the internal state of Hermes MCP client and transport processes, useful for debugging and introspection purposes. ## Features * Retrieves and formats client state information * Displays transport state (SSE or STDIO) * Pretty-prints nested data structures * Shows connection status and diagnostics * Highlights pending requests with timing information ## Usage This module is primarily used by the interactive shell's `show_state` command to provide a human-readable view of the system's internal state. ```elixir # From the commands module: Mix.Interactive.State.print_state(client_pid) ``` """ alias Hermes.Client.Request alias Hermes.Transport.SSE alias Hermes.Transport.STDIO alias Mix.Interactive.UI @doc """ Prints the formatted state of the client and its associated transport. ## Parameters * `client` - The client process to inspect """ @spec print_state(GenServer.server()) :: :ok def print_state(client) do client_state = :sys.get_state(client) transport = get_transport_info(client_state) verbose = System.get_env("HERMES_VERBOSE") == "1" print_client_state(client, client_state, verbose) print_transport_state(transport.pid, transport.layer, verbose) end defp get_transport_info(client_state) do transport_info = client_state.transport layer = if is_map(transport_info), do: transport_info[:layer], else: transport_info transport_pid = if is_map(transport_info) and Map.has_key?(transport_info, :name) and is_pid(transport_info.name) do transport_info.name else Process.whereis(layer) end %{layer: layer, pid: transport_pid} end defp print_client_state(client, state, verbose) do IO.puts("\n#{UI.colors().success}Client State (#{inspect(client)}):#{UI.colors().reset}") print_protocol_info(state) print_client_info(state, verbose) print_server_capabilities(state, verbose) print_server_info(state, verbose) print_pending_requests(state.pending_requests, verbose) if verbose do print_callbacks_info(state) end end defp print_protocol_info(state) do IO.puts(" #{UI.colors().info}Protocol Version:#{UI.colors().reset} #{state.protocol_version}") end defp print_client_info(state, _verbose) do IO.puts(" #{UI.colors().info}Client Info:#{UI.colors().reset}") print_map(state.client_info, 4) end defp print_server_capabilities(state, verbose) do if state.server_capabilities do IO.puts(" #{UI.colors().info}Server Capabilities:#{UI.colors().reset}") if verbose do # Show full details in verbose mode print_map(state.server_capabilities, 4) else # Show summary in normal mode capability_keys = Map.keys(state.server_capabilities) IO.puts(" #{inspect(capability_keys)}") end else IO.puts(" #{UI.colors().warning}Server Capabilities: Not yet established#{UI.colors().reset}") end end defp print_server_info(state, _verbose) do if state.server_info do IO.puts(" #{UI.colors().info}Server Info:#{UI.colors().reset}") print_map(state.server_info, 4) else IO.puts(" #{UI.colors().warning}Server Info: Not yet established#{UI.colors().reset}") end end defp print_pending_requests(pending_requests, verbose) do request_count = map_size(pending_requests) if request_count == 0 do IO.puts(" #{UI.colors().info}Pending Requests:#{UI.colors().reset} None") else IO.puts(" #{UI.colors().info}Pending Requests (#{request_count}):#{UI.colors().reset}") Enum.each(pending_requests, &print_request(&1, verbose)) end end defp print_request({id, request}, verbose) do elapsed_ms = Request.elapsed_time(request) IO.puts(" #{UI.colors().command}#{id}#{UI.colors().reset} - Method: #{request.method}, Elapsed: #{elapsed_ms}ms") if verbose do print_request_details(request) end end defp print_request_details(request) do IO.puts(" Started at: #{inspect(request.start_time)}") IO.puts(" From: #{inspect(request.from)}") end defp print_callbacks_info(state) do IO.puts(" #{UI.colors().info}Callbacks:#{UI.colors().reset}") # Progress callbacks progress_count = map_size(state.progress_callbacks) if progress_count > 0 do IO.puts(" #{UI.colors().info}Progress Callbacks (#{progress_count}):#{UI.colors().reset}") Enum.each(state.progress_callbacks, fn {token, _callback} -> IO.puts(" #{UI.colors().command}#{token}#{UI.colors().reset}") end) else IO.puts(" #{UI.colors().info}Progress Callbacks:#{UI.colors().reset} None") end # Log callback if state.log_callback do IO.puts(" #{UI.colors().info}Log Callback:#{UI.colors().reset} Configured") else IO.puts(" #{UI.colors().info}Log Callback:#{UI.colors().reset} None") end end defp print_transport_state(nil, transport_layer, _verbose) do IO.puts( "\n#{UI.colors().error}Transport State (#{inspect(transport_layer)}):#{UI.colors().reset} Not available (process not found)" ) end defp print_transport_state(transport_pid, transport_layer, verbose) when is_pid(transport_pid) do if Process.alive?(transport_pid) do transport_state = :sys.get_state(transport_pid) case transport_layer do SSE -> print_sse_transport_state(transport_pid, transport_state, verbose) STDIO -> print_stdio_transport_state(transport_pid, transport_state, verbose) _ -> print_unknown_transport_state(transport_pid, transport_state, verbose) end else IO.puts( "\n#{UI.colors().error}Transport State (#{inspect(transport_layer)}):#{UI.colors().reset} Not available (process not running)" ) end end defp print_transport_state(_transport_pid, transport_layer, _verbose) do IO.puts( "\n#{UI.colors().error}Transport State (#{inspect(transport_layer)}):#{UI.colors().reset} Not available (invalid process identifier)" ) end defp print_sse_transport_state(pid, state, verbose) do IO.puts("\n#{UI.colors().success}SSE Transport State (#{inspect(pid)}):#{UI.colors().reset}") IO.puts(" #{UI.colors().info}Server URL:#{UI.colors().reset} #{state[:server_url]}") IO.puts(" #{UI.colors().info}SSE URL:#{UI.colors().reset} #{state[:sse_url]}") print_sse_connection_status(state) print_sse_stream_task(state) if verbose do # Print additional transport details in verbose mode if map_size(state[:headers] || %{}) > 0 do IO.puts(" #{UI.colors().info}Headers:#{UI.colors().reset}") print_map(state[:headers], 4) end if state[:transport_opts] do IO.puts(" #{UI.colors().info}Transport Options:#{UI.colors().reset} #{inspect(state[:transport_opts])}") end if state[:http_options] do IO.puts(" #{UI.colors().info}HTTP Options:#{UI.colors().reset} #{inspect(state[:http_options])}") end end end defp print_sse_connection_status(state) do if state[:message_url] do IO.puts(" #{UI.colors().info}Message URL:#{UI.colors().reset} #{state[:message_url]}") IO.puts(" #{UI.colors().success}Status:#{UI.colors().reset} Connected") else IO.puts(" #{UI.colors().warning}Status:#{UI.colors().reset} Connecting/Not connected") end end defp print_sse_stream_task(state) do if state[:stream_task] do task = state[:stream_task] status = if Process.alive?(task.pid), do: "alive", else: "dead" IO.puts(" #{UI.colors().info}Stream Task:#{UI.colors().reset} #{inspect(task.pid)} (#{status})") end end defp print_stdio_transport_state(pid, state, verbose) do IO.puts("\n#{UI.colors().success}STDIO Transport State (#{inspect(pid)}):#{UI.colors().reset}") IO.puts(" #{UI.colors().info}Command:#{UI.colors().reset} #{state.command}") print_stdio_args(state) print_stdio_connection_status(state) if verbose do # Print additional transport details in verbose mode if state.cwd do IO.puts(" #{UI.colors().info}Working Directory:#{UI.colors().reset} #{state.cwd}") end if state.env do IO.puts(" #{UI.colors().info}Environment:#{UI.colors().reset}") print_map(state.env, 4) end end end defp print_stdio_args(state) do if state.args do IO.puts(" #{UI.colors().info}Args:#{UI.colors().reset} #{inspect(state.args)}") end end defp print_stdio_connection_status(state) do if state.port do status = if Port.info(state.port), do: "open", else: "closed" IO.puts(" #{UI.colors().info}Port:#{UI.colors().reset} #{inspect(state.port)} (#{status})") IO.puts(" #{UI.colors().success}Status:#{UI.colors().reset} Connected") else IO.puts(" #{UI.colors().warning}Status:#{UI.colors().reset} Not connected") end end defp print_unknown_transport_state(pid, state, verbose) do IO.puts("\n#{UI.colors().success}Unknown Transport State (#{inspect(pid)}):#{UI.colors().reset}") if verbose do # In verbose mode, show the full state with pretty printing IO.puts(" #{inspect(state, pretty: true, limit: 50)}") else # In normal mode, show a summary IO.puts(" #{inspect(state, pretty: true, limit: 5)}") end end defp print_map(map, indent_level) when is_map(map) do Enum.each(map, fn {key, value} -> print_map_entry(key, value, indent_level) end) end defp print_map_entry(key, value, indent_level) do indent = String.duplicate(" ", indent_level) cond do is_map(value) and map_size(value) > 0 -> IO.puts("#{indent}#{UI.colors().command}#{key}:#{UI.colors().reset}") print_map(value, indent_level + 2) is_list(value) and value != [] -> IO.puts("#{indent}#{UI.colors().command}#{key}:#{UI.colors().reset}") print_list(value, indent_level + 2) true -> IO.puts("#{indent}#{UI.colors().command}#{key}:#{UI.colors().reset} #{inspect(value)}") end end defp print_list(list, indent_level) do indent = String.duplicate(" ", indent_level) Enum.each(list, fn item -> cond do is_map(item) and map_size(item) > 0 -> IO.puts("#{indent}-") print_map(item, indent_level + 2) is_list(item) and item != [] -> IO.puts("#{indent}- [...]") print_list(item, indent_level + 2) true -> IO.puts("#{indent}- #{inspect(item)}") end end) end end