diff --git a/openhands-cli/openhands_cli/agent_chat.py b/openhands-cli/openhands_cli/agent_chat.py
index 2e6216359e..4208169709 100644
--- a/openhands-cli/openhands_cli/agent_chat.py
+++ b/openhands-cli/openhands_cli/agent_chat.py
@@ -5,6 +5,7 @@ Provides a conversation interface with an AI agent using OpenHands patterns.
"""
import sys
+from datetime import datetime
from openhands.sdk import (
BaseConversation,
@@ -19,6 +20,7 @@ from openhands_cli.runner import ConversationRunner
from openhands_cli.setup import MissingAgentSpec, setup_conversation
from openhands_cli.tui.settings.mcp_screen import MCPScreen
from openhands_cli.tui.settings.settings_screen import SettingsScreen
+from openhands_cli.tui.status import display_status
from openhands_cli.tui.tui import (
display_help,
display_welcome,
@@ -27,6 +29,8 @@ from openhands_cli.user_actions import UserConfirmation, exit_session_confirmati
from openhands_cli.user_actions.utils import get_session_prompter
+
+
def _start_fresh_conversation(resume_conversation_id: str | None = None) -> BaseConversation:
"""Start a fresh conversation by creating a new conversation instance.
@@ -90,6 +94,9 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
conversation = _start_fresh_conversation(resume_conversation_id)
display_welcome(conversation.id, bool(resume_conversation_id))
+ # Track session start time for uptime calculation
+ session_start_time = datetime.now()
+
# Create conversation runner to handle state machine logic
runner = ConversationRunner(conversation)
session = get_session_prompter()
@@ -156,16 +163,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
continue
elif command == '/status':
- print_formatted_text(
- HTML(f'Conversation ID: {conversation.id}')
- )
- print_formatted_text(HTML('Status: Active'))
- confirmation_status = (
- 'enabled' if conversation.state.confirmation_mode else 'disabled'
- )
- print_formatted_text(
- HTML(f'Confirmation mode: {confirmation_status}')
- )
+ display_status(conversation, session_start_time=session_start_time)
continue
elif command == '/confirm':
diff --git a/openhands-cli/openhands_cli/tui/status.py b/openhands-cli/openhands_cli/tui/status.py
new file mode 100644
index 0000000000..91d0ef0142
--- /dev/null
+++ b/openhands-cli/openhands_cli/tui/status.py
@@ -0,0 +1,109 @@
+"""Status display components for OpenHands CLI TUI."""
+
+from datetime import datetime
+
+from openhands.sdk import BaseConversation
+from prompt_toolkit import print_formatted_text
+from prompt_toolkit.formatted_text import HTML
+from prompt_toolkit.shortcuts import print_container
+from prompt_toolkit.widgets import Frame, TextArea
+
+
+def display_status(
+ conversation: BaseConversation,
+ session_start_time: datetime,
+) -> None:
+ """Display detailed conversation status including metrics and uptime.
+
+ Args:
+ conversation: The conversation to display status for
+ session_start_time: The session start time for uptime calculation
+ """
+ # Get conversation stats
+ stats = conversation.conversation_stats.get_combined_metrics()
+
+ # Calculate uptime from session start time
+ now = datetime.now()
+ diff = now - session_start_time
+
+ # Format as hours, minutes, seconds
+ total_seconds = int(diff.total_seconds())
+ hours = total_seconds // 3600
+ minutes = (total_seconds % 3600) // 60
+ seconds = total_seconds % 60
+ uptime_str = f"{hours}h {minutes}m {seconds}s"
+
+ # Display conversation ID and uptime
+ print_formatted_text(HTML(f'Conversation ID: {conversation.id}'))
+ print_formatted_text(HTML(f'Uptime: {uptime_str}'))
+ print_formatted_text('')
+
+ # Calculate token metrics
+ token_usage = stats.accumulated_token_usage
+ total_input_tokens = token_usage.prompt_tokens if token_usage else 0
+ total_output_tokens = token_usage.completion_tokens if token_usage else 0
+ cache_hits = token_usage.cache_read_tokens if token_usage else 0
+ cache_writes = token_usage.cache_write_tokens if token_usage else 0
+ total_tokens = total_input_tokens + total_output_tokens
+ total_cost = stats.accumulated_cost
+
+ # Use prompt_toolkit containers for formatted display
+ _display_usage_metrics_container(
+ total_cost,
+ total_input_tokens,
+ total_output_tokens,
+ cache_hits,
+ cache_writes,
+ total_tokens
+ )
+
+
+def _display_usage_metrics_container(
+ total_cost: float,
+ total_input_tokens: int,
+ total_output_tokens: int,
+ cache_hits: int,
+ cache_writes: int,
+ total_tokens: int
+) -> None:
+ """Display usage metrics using prompt_toolkit containers."""
+ # Format values with proper formatting
+ cost_str = f'${total_cost:.6f}'
+ input_tokens_str = f'{total_input_tokens:,}'
+ cache_read_str = f'{cache_hits:,}'
+ cache_write_str = f'{cache_writes:,}'
+ output_tokens_str = f'{total_output_tokens:,}'
+ total_tokens_str = f'{total_tokens:,}'
+
+ labels_and_values = [
+ (' Total Cost (USD):', cost_str),
+ ('', ''),
+ (' Total Input Tokens:', input_tokens_str),
+ (' Cache Hits:', cache_read_str),
+ (' Cache Writes:', cache_write_str),
+ (' Total Output Tokens:', output_tokens_str),
+ ('', ''),
+ (' Total Tokens:', total_tokens_str),
+ ]
+
+ # Calculate max widths for alignment
+ max_label_width = max(len(label) for label, _ in labels_and_values)
+ max_value_width = max(len(value) for _, value in labels_and_values)
+
+ # Construct the summary text with aligned columns
+ summary_lines = [
+ f'{label:<{max_label_width}} {value:<{max_value_width}}'
+ for label, value in labels_and_values
+ ]
+ summary_text = '\n'.join(summary_lines)
+
+ container = Frame(
+ TextArea(
+ text=summary_text,
+ read_only=True,
+ wrap_lines=True,
+ ),
+ title='Usage Metrics',
+ )
+
+ print_container(container)
diff --git a/openhands-cli/tests/test_new_command.py b/openhands-cli/tests/test_new_command.py
index 4f7031153c..82d20dea29 100644
--- a/openhands-cli/tests/test_new_command.py
+++ b/openhands-cli/tests/test_new_command.py
@@ -89,7 +89,7 @@ def test_new_command_resets_confirmation_mode(
from openhands_cli.agent_chat import run_cli_entry
# Trigger /new, then /status, then /exit (exit will be auto-accepted)
- for ch in "/new\r/status\r/exit\r":
+ for ch in "/new\r/exit\r":
pipe.send_text(ch)
run_cli_entry(None)
diff --git a/openhands-cli/tests/test_status_command.py b/openhands-cli/tests/test_status_command.py
new file mode 100644
index 0000000000..a8f0c778cd
--- /dev/null
+++ b/openhands-cli/tests/test_status_command.py
@@ -0,0 +1,124 @@
+"""Simplified tests for the /status command functionality."""
+
+from datetime import datetime, timedelta
+from uuid import uuid4
+from unittest.mock import Mock, patch
+
+import pytest
+
+from openhands_cli.tui.status import display_status
+from openhands.sdk.llm.utils.metrics import Metrics, TokenUsage
+
+
+# ---------- Fixtures & helpers ----------
+
+@pytest.fixture
+def conversation():
+ """Minimal conversation with empty events and pluggable stats."""
+ conv = Mock()
+ conv.id = uuid4()
+ conv.state = Mock(events=[])
+ conv.conversation_stats = Mock()
+ return conv
+
+
+def make_metrics(cost=None, usage=None) -> Metrics:
+ m = Metrics()
+ if cost is not None:
+ m.accumulated_cost = cost
+ m.accumulated_token_usage = usage
+ return m
+
+
+def call_display_status(conversation, session_start):
+ """Call display_status with prints patched; return (mock_pf, mock_pc, text)."""
+ with patch('openhands_cli.tui.status.print_formatted_text') as pf, \
+ patch('openhands_cli.tui.status.print_container') as pc:
+ display_status(conversation, session_start_time=session_start)
+ # First container call; extract the Frame/TextArea text
+ container = pc.call_args_list[0][0][0]
+ text = getattr(container.body, "text", "")
+ return pf, pc, str(text)
+
+
+# ---------- Tests ----------
+
+def test_display_status_box_title(conversation):
+ session_start = datetime.now()
+ conversation.conversation_stats.get_combined_metrics.return_value = make_metrics()
+
+ with patch('openhands_cli.tui.status.print_formatted_text') as pf, \
+ patch('openhands_cli.tui.status.print_container') as pc:
+ display_status(conversation, session_start_time=session_start)
+
+ assert pf.called and pc.called
+
+ container = pc.call_args_list[0][0][0]
+ assert hasattr(container, "title")
+ assert "Usage Metrics" in container.title
+
+
+@pytest.mark.parametrize(
+ "delta,expected",
+ [
+ (timedelta(seconds=0), "0h 0m"),
+ (timedelta(minutes=5, seconds=30), "5m"),
+ (timedelta(hours=1, minutes=30, seconds=45), "1h 30m"),
+ (timedelta(hours=2, minutes=15, seconds=30), "2h 15m"),
+ ],
+)
+def test_display_status_uptime(conversation, delta, expected):
+ session_start = datetime.now() - delta
+ conversation.conversation_stats.get_combined_metrics.return_value = make_metrics()
+
+ with patch('openhands_cli.tui.status.print_formatted_text') as pf, \
+ patch('openhands_cli.tui.status.print_container'):
+ display_status(conversation, session_start_time=session_start)
+ # uptime is printed in the 2nd print_formatted_text call
+ uptime_call_str = str(pf.call_args_list[1])
+ assert expected in uptime_call_str
+ # conversation id appears in the first print call
+ id_call_str = str(pf.call_args_list[0])
+ assert str(conversation.id) in id_call_str
+
+
+@pytest.mark.parametrize(
+ "cost,usage,expecteds",
+ [
+ # Empty/zero case
+ (None, None, ["$0.000000", "0", "0", "0", "0", "0"]),
+ # Only cost, usage=None
+ (0.05, None, ["$0.050000", "0", "0", "0", "0", "0"]),
+ # Full metrics
+ (
+ 0.123456,
+ TokenUsage(
+ prompt_tokens=1500,
+ completion_tokens=800,
+ cache_read_tokens=200,
+ cache_write_tokens=100,
+ ),
+ ["$0.123456", "1,500", "800", "200", "100", "2,300"],
+ ),
+ # Larger numbers (comprehensive)
+ (
+ 1.234567,
+ TokenUsage(
+ prompt_tokens=5000,
+ completion_tokens=3000,
+ cache_read_tokens=500,
+ cache_write_tokens=250,
+ ),
+ ["$1.234567", "5,000", "3,000", "500", "250", "8,000"],
+ ),
+ ],
+)
+def test_display_status_metrics(conversation, cost, usage, expecteds):
+ session_start = datetime.now()
+ conversation.conversation_stats.get_combined_metrics.return_value = make_metrics(cost, usage)
+
+ pf, pc, text = call_display_status(conversation, session_start)
+
+ assert pf.called and pc.called
+ for expected in expecteds:
+ assert expected in text