Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions dimos/utils/cli/agentspy/agentspy.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from textual.widgets import Footer, RichLog

from dimos.protocol.pubsub.lcmpubsub import PickleLCM
from dimos.utils.cli import theme

# Type alias for all message types we might receive
AnyMessage = Union[SystemMessage, ToolMessage, AIMessage, HumanMessage]
Expand Down Expand Up @@ -137,23 +138,25 @@ def format_message_content(msg: AnyMessage) -> str:
class AgentSpyApp(App):
"""TUI application for monitoring agent messages."""

CSS = """
Screen {
CSS_PATH = theme.CSS_PATH

CSS = f"""
Screen {{
layout: vertical;
background: black;
}
background: {theme.BACKGROUND};
}}

RichLog {
RichLog {{
height: 1fr;
border: none;
background: black;
background: {theme.BACKGROUND};
padding: 0 1;
}
}}

Footer {
Footer {{
dock: bottom;
height: 1;
}
}}
"""

BINDINGS = [
Expand Down
91 changes: 91 additions & 0 deletions dimos/utils/cli/dimos.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* DimOS Base Theme for Textual CLI Applications
* Based on colors.json - Official DimOS color palette
*/

/* Base Color Palette (from colors.json) */
$black: #0b0f0f;
$red: #ff0000;
$green: #00eeee;
$yellow: #ffcc00;
$blue: #5c9ff0;
$purple: #00eeee;
$cyan: #00eeee;
$white: #b5e4f4;

/* Bright Colors */
$bright-black: #404040;
$bright-red: #ff0000;
$bright-green: #00eeee;
$bright-yellow: #f2ea8c;
$bright-blue: #8cbdf2;
$bright-purple: #00eeee;
$bright-cyan: #00eeee;
$bright-white: #ffffff;

/* Core Theme Colors */
$background: #0b0f0f;
$foreground: #b5e4f4;
$cursor: #00eeee;

/* Semantic Aliases */
$bg: $black;
$border: $cyan;
$accent: $white;
$dim: $bright-black;
$timestamp: $bright-white;

/* Message Type Colors */
$system: $red;
$agent: #88ff88;
$tool: $cyan;
$tool-result: $yellow;
$human: $bright-white;

/* Status Colors */
$success: $green;
$error: $red;
$warning: $yellow;
$info: $cyan;

/* Base Screen */
Screen {
background: $bg;
}

/* Default Container */
Container {
background: $bg;
}

/* Input Widget */
Input {
background: $bg;
border: solid $border;
color: $accent;
}

Input:focus {
border: solid $border;
}

/* RichLog Widget */
RichLog {
background: $bg;
border: solid $border;
}

/* Button Widget */
Button {
background: $bg;
border: solid $border;
color: $accent;
}

Button:hover {
background: $dim;
border: solid $accent;
}

Button:focus {
border: double $accent;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,54 @@
from typing import Optional

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolCall, ToolMessage
from rich.highlighter import JSONHighlighter
from rich.theme import Theme
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container
from textual.events import Key
from textual.widgets import Input, RichLog

from dimos.core import pLCMTransport
from dimos.utils.cli import theme
from dimos.utils.generic import truncate_display_string


# Custom theme for JSON highlighting
JSON_THEME = Theme(
{
"json.key": theme.CYAN,
"json.str": theme.ACCENT,
"json.number": theme.ACCENT,
"json.bool_true": theme.ACCENT,
"json.bool_false": theme.ACCENT,
"json.null": theme.DIM,
"json.brace": theme.BRIGHT_WHITE,
}
)


class HumanCLIApp(App):
"""IRC-like interface for interacting with DimOS agents."""

CSS = """
Screen {
background: black;
}

#chat-container {
CSS_PATH = theme.CSS_PATH

CSS = f"""
Screen {{
background: {theme.BACKGROUND};
}}

#chat-container {{
height: 1fr;
background: black;
}

Input {
background: black;
}}

RichLog {{
scrollbar-size: 0 0;
}}

Input {{
dock: bottom;
}

RichLog {
background: black;
}
}}
"""

BINDINGS = [
Expand Down Expand Up @@ -79,16 +96,32 @@ def compose(self) -> ComposeResult:

def on_mount(self) -> None:
"""Initialize the app when mounted."""
self.theme = "flexoki"
self._running = True

# Apply custom JSON theme to app console
self.console.push_theme(JSON_THEME)

# Set custom highlighter for RichLog
self.chat_log.highlighter = JSONHighlighter()

# Start subscription thread
self._subscription_thread = threading.Thread(target=self._subscribe_to_agent, daemon=True)
self._subscription_thread.start()

# Focus on input
self.input_widget.focus()

# Display ASCII art banner
ascii_art = """
██████╗ ██╗███╗ ███╗███████╗███╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██╗
██╔══██╗██║████╗ ████║██╔════╝████╗ ██║██╔════╝██║██╔═══██╗████╗ ██║██╔══██╗██║
██║ ██║██║██╔████╔██║█████╗ ██╔██╗ ██║███████╗██║██║ ██║██╔██╗ ██║███████║██║
██║ ██║██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║╚════██║██║██║ ██║██║╚██╗██║██╔══██║██║
██████╔╝██║██║ ╚═╝ ██║███████╗██║ ╚████║███████║██║╚██████╔╝██║ ╚████║██║ ██║███████╗
╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝
"""
self.chat_log.write(f"[{theme.ACCENT}]{ascii_art}[/{theme.ACCENT}]")

# Welcome message
self._add_system_message("Connected to DimOS Agent Interface")

Expand All @@ -111,33 +144,39 @@ def receive_msg(msg):
timestamp,
"system",
truncate_display_string(msg.content, 1000),
"red",
theme.YELLOW,
)
elif isinstance(msg, AIMessage):
content = msg.content or ""
tool_calls = msg.additional_kwargs.get("tool_calls", [])

# Display the main content first
if content:
self.call_from_thread(self._add_message, timestamp, "agent", content, "orange")
self.call_from_thread(
self._add_message, timestamp, "agent", content, theme.AGENT
)

# Display tool calls separately with different formatting
if tool_calls:
for tc in tool_calls:
tool_info = self._format_tool_call(tc)
self.call_from_thread(
self._add_message, timestamp, "tool", tool_info, "cyan"
self._add_message, timestamp, "tool", tool_info, theme.TOOL
)

# If neither content nor tool calls, show a placeholder
if not content and not tool_calls:
self.call_from_thread(
self._add_message, timestamp, "agent", "<no response>", "dim"
self._add_message, timestamp, "agent", "<no response>", theme.DIM
)
elif isinstance(msg, ToolMessage):
self.call_from_thread(self._add_message, timestamp, "tool", msg.content, "yellow")
self.call_from_thread(
self._add_message, timestamp, "tool", msg.content, theme.TOOL_RESULT
)
elif isinstance(msg, HumanMessage):
self.call_from_thread(self._add_message, timestamp, "human", msg.content, "green")
self.call_from_thread(
self._add_message, timestamp, "human", msg.content, theme.HUMAN
)

self.agent_transport.subscribe(receive_msg)

Expand All @@ -156,9 +195,9 @@ def _add_message(self, timestamp: str, sender: str, content: str, color: str) ->
time_parts = timestamp.split(":")
if len(time_parts) == 3:
# Format as HH:MM:SS with colored colons
timestamp_formatted = f" [dim white]{time_parts[0]}[/dim white][bright_black]:[/bright_black][dim white]{time_parts[1]}[/dim white][bright_black]:[/bright_black][dim white]{time_parts[2]}[/dim white]"
timestamp_formatted = f" [{theme.TIMESTAMP}]{time_parts[0]}:{time_parts[1]}:{time_parts[2]}[/{theme.TIMESTAMP}]"
else:
timestamp_formatted = f" [dim white]{timestamp}[/dim white]"
timestamp_formatted = f" [{theme.TIMESTAMP}]{timestamp}[/{theme.TIMESTAMP}]"

# Format sender with consistent width
sender_formatted = f"[{color}]{sender:>8}[/{color}]"
Expand Down Expand Up @@ -206,7 +245,7 @@ def _add_message(self, timestamp: str, sender: str, content: str, color: str) ->
def _add_system_message(self, content: str) -> None:
"""Add a system message to the chat."""
timestamp = datetime.now().strftime("%H:%M:%S")
self._add_message(timestamp, "system", content, "red")
self._add_message(timestamp, "system", content, theme.YELLOW)

def on_key(self, event: Key) -> None:
"""Handle key events."""
Expand Down
Loading
Loading