a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
342 lines
10 KiB
Python
Executable File
342 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
jcode Live Monitor - Real-time activity dashboard
|
|
|
|
Connects to jcode's debug socket and displays live streaming events.
|
|
Run jcode serve in one terminal, then this monitor in another.
|
|
|
|
Usage: ./jcode_monitor.py [--socket PATH]
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import socket
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
# ANSI color codes
|
|
class Colors:
|
|
RESET = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
DIM = "\033[2m"
|
|
|
|
RED = "\033[31m"
|
|
GREEN = "\033[32m"
|
|
YELLOW = "\033[33m"
|
|
BLUE = "\033[34m"
|
|
MAGENTA = "\033[35m"
|
|
CYAN = "\033[36m"
|
|
WHITE = "\033[37m"
|
|
|
|
BG_BLUE = "\033[44m"
|
|
|
|
# Clear screen and move cursor
|
|
CLEAR = "\033[2J\033[H"
|
|
CLEAR_LINE = "\033[2K"
|
|
|
|
|
|
@dataclass
|
|
class MonitorState:
|
|
"""Current state of the monitor"""
|
|
connected: bool = False
|
|
session_id: str = ""
|
|
is_processing: bool = False
|
|
input_tokens: int = 0
|
|
output_tokens: int = 0
|
|
current_text: str = ""
|
|
active_tools: dict = field(default_factory=dict) # id -> name
|
|
tool_history: list = field(default_factory=list) # recent tool completions
|
|
events_received: int = 0
|
|
last_event_time: float = 0
|
|
errors: list = field(default_factory=list)
|
|
|
|
|
|
def get_socket_path() -> str:
|
|
"""Get the jcode debug socket path"""
|
|
runtime_dir = os.environ.get("XDG_RUNTIME_DIR", f"/run/user/{os.getuid()}")
|
|
return os.path.join(runtime_dir, "jcode-debug.sock")
|
|
|
|
|
|
def connect_to_socket(path: str) -> Optional[socket.socket]:
|
|
"""Connect to the Unix socket"""
|
|
try:
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.connect(path)
|
|
sock.setblocking(False)
|
|
return sock
|
|
except Exception as e:
|
|
return None
|
|
|
|
|
|
def send_request(sock: socket.socket, request: dict) -> bool:
|
|
"""Send a JSON request to the socket"""
|
|
try:
|
|
data = json.dumps(request) + "\n"
|
|
sock.send(data.encode())
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
|
|
def read_events(sock: socket.socket) -> list:
|
|
"""Read available events from socket (non-blocking)"""
|
|
events = []
|
|
buffer = ""
|
|
|
|
try:
|
|
while True:
|
|
data = sock.recv(4096)
|
|
if not data:
|
|
break
|
|
buffer += data.decode()
|
|
|
|
while "\n" in buffer:
|
|
line, buffer = buffer.split("\n", 1)
|
|
if line.strip():
|
|
try:
|
|
events.append(json.loads(line))
|
|
except json.JSONDecodeError:
|
|
pass
|
|
except BlockingIOError:
|
|
pass
|
|
except:
|
|
pass
|
|
|
|
return events
|
|
|
|
|
|
def format_tokens(n: int) -> str:
|
|
"""Format token count with color based on size"""
|
|
if n == 0:
|
|
return f"{Colors.DIM}0{Colors.RESET}"
|
|
elif n < 1000:
|
|
return f"{Colors.GREEN}{n}{Colors.RESET}"
|
|
elif n < 10000:
|
|
return f"{Colors.YELLOW}{n:,}{Colors.RESET}"
|
|
else:
|
|
return f"{Colors.RED}{n:,}{Colors.RESET}"
|
|
|
|
|
|
def format_tool(name: str, status: str = "active") -> str:
|
|
"""Format a tool name with appropriate color"""
|
|
if status == "active":
|
|
return f"{Colors.CYAN}{Colors.BOLD}{name}{Colors.RESET}"
|
|
elif status == "done":
|
|
return f"{Colors.GREEN}{name}{Colors.RESET}"
|
|
elif status == "error":
|
|
return f"{Colors.RED}{name}{Colors.RESET}"
|
|
return name
|
|
|
|
|
|
def truncate(s: str, max_len: int) -> str:
|
|
"""Truncate string with ellipsis"""
|
|
if len(s) <= max_len:
|
|
return s
|
|
return s[:max_len-3] + "..."
|
|
|
|
|
|
def render_dashboard(state: MonitorState, width: int = 80):
|
|
"""Render the monitoring dashboard"""
|
|
lines = []
|
|
|
|
# Header
|
|
header = f" JCODE MONITOR "
|
|
padding = (width - len(header)) // 2
|
|
lines.append(f"{Colors.BG_BLUE}{Colors.WHITE}{Colors.BOLD}{' ' * padding}{header}{' ' * padding}{Colors.RESET}")
|
|
lines.append("")
|
|
|
|
# Connection status
|
|
if state.connected:
|
|
status = f"{Colors.GREEN}CONNECTED{Colors.RESET}"
|
|
session = f" | Session: {Colors.DIM}{state.session_id[:8]}...{Colors.RESET}" if state.session_id else ""
|
|
else:
|
|
status = f"{Colors.RED}DISCONNECTED{Colors.RESET}"
|
|
session = ""
|
|
lines.append(f" Status: {status}{session}")
|
|
|
|
# Processing state
|
|
if state.is_processing:
|
|
proc = f"{Colors.YELLOW}{Colors.BOLD}PROCESSING{Colors.RESET}"
|
|
else:
|
|
proc = f"{Colors.DIM}idle{Colors.RESET}"
|
|
lines.append(f" State: {proc}")
|
|
lines.append("")
|
|
|
|
# Token usage
|
|
lines.append(f" {Colors.BOLD}Tokens{Colors.RESET}")
|
|
lines.append(f" Input: {format_tokens(state.input_tokens)}")
|
|
lines.append(f" Output: {format_tokens(state.output_tokens)}")
|
|
lines.append("")
|
|
|
|
# Active tools
|
|
lines.append(f" {Colors.BOLD}Active Tools{Colors.RESET}")
|
|
if state.active_tools:
|
|
for tool_id, tool_name in state.active_tools.items():
|
|
lines.append(f" {Colors.CYAN}>{Colors.RESET} {format_tool(tool_name)}")
|
|
else:
|
|
lines.append(f" {Colors.DIM}(none){Colors.RESET}")
|
|
lines.append("")
|
|
|
|
# Recent tool completions
|
|
lines.append(f" {Colors.BOLD}Recent Tools{Colors.RESET}")
|
|
if state.tool_history:
|
|
for item in state.tool_history[-5:]:
|
|
name, success, output = item
|
|
status = "done" if success else "error"
|
|
output_preview = truncate(output.replace("\n", " "), 40)
|
|
lines.append(f" {format_tool(name, status)}: {Colors.DIM}{output_preview}{Colors.RESET}")
|
|
else:
|
|
lines.append(f" {Colors.DIM}(none){Colors.RESET}")
|
|
lines.append("")
|
|
|
|
# Current streaming text
|
|
lines.append(f" {Colors.BOLD}Streaming Text{Colors.RESET}")
|
|
if state.current_text:
|
|
# Show last few lines of streaming text
|
|
text_lines = state.current_text.split("\n")[-4:]
|
|
for tl in text_lines:
|
|
lines.append(f" {Colors.WHITE}{truncate(tl, width-6)}{Colors.RESET}")
|
|
else:
|
|
lines.append(f" {Colors.DIM}(waiting...){Colors.RESET}")
|
|
lines.append("")
|
|
|
|
# Stats
|
|
lines.append(f" {Colors.DIM}Events: {state.events_received} | Last: {time.time() - state.last_event_time:.1f}s ago{Colors.RESET}")
|
|
|
|
# Errors
|
|
if state.errors:
|
|
lines.append("")
|
|
lines.append(f" {Colors.RED}{Colors.BOLD}Errors{Colors.RESET}")
|
|
for err in state.errors[-3:]:
|
|
lines.append(f" {Colors.RED}{truncate(err, width-6)}{Colors.RESET}")
|
|
|
|
# Print with clear
|
|
print(Colors.CLEAR, end="")
|
|
print("\n".join(lines))
|
|
|
|
|
|
def process_event(event: dict, state: MonitorState):
|
|
"""Process a single event and update state"""
|
|
state.events_received += 1
|
|
state.last_event_time = time.time()
|
|
|
|
event_type = event.get("type", "")
|
|
|
|
if event_type == "ack":
|
|
state.is_processing = True
|
|
|
|
elif event_type == "text_delta":
|
|
state.current_text += event.get("text", "")
|
|
# Keep last 2000 chars
|
|
if len(state.current_text) > 2000:
|
|
state.current_text = state.current_text[-2000:]
|
|
|
|
elif event_type == "tool_start":
|
|
tool_id = event.get("id", "")
|
|
tool_name = event.get("name", "unknown")
|
|
state.active_tools[tool_id] = tool_name
|
|
|
|
elif event_type == "tool_exec":
|
|
pass # Tool is executing, still active
|
|
|
|
elif event_type == "tool_done":
|
|
tool_id = event.get("id", "")
|
|
tool_name = event.get("name", "unknown")
|
|
output = event.get("output", "")
|
|
error = event.get("error")
|
|
|
|
# Remove from active
|
|
state.active_tools.pop(tool_id, None)
|
|
|
|
# Add to history
|
|
state.tool_history.append((tool_name, error is None, output[:100] if output else "(empty)"))
|
|
# Keep last 20
|
|
state.tool_history = state.tool_history[-20:]
|
|
|
|
elif event_type == "tokens":
|
|
state.input_tokens = event.get("input", 0)
|
|
state.output_tokens = event.get("output", 0)
|
|
|
|
elif event_type == "done":
|
|
state.is_processing = False
|
|
state.current_text = "" # Clear for next turn
|
|
|
|
elif event_type == "error":
|
|
state.errors.append(event.get("message", "unknown error"))
|
|
state.errors = state.errors[-10:]
|
|
|
|
elif event_type == "pong":
|
|
pass # Health check response
|
|
|
|
elif event_type == "state":
|
|
state.session_id = event.get("session_id", "")
|
|
state.is_processing = event.get("is_processing", False)
|
|
|
|
elif event_type == "session":
|
|
state.session_id = event.get("session_id", "")
|
|
|
|
|
|
def main():
|
|
"""Main monitor loop"""
|
|
socket_path = sys.argv[1] if len(sys.argv) > 1 else get_socket_path()
|
|
|
|
print(f"Connecting to {socket_path}...")
|
|
|
|
state = MonitorState()
|
|
sock = None
|
|
request_id = 1
|
|
last_ping = 0
|
|
|
|
while True:
|
|
try:
|
|
# Connect if needed
|
|
if sock is None:
|
|
sock = connect_to_socket(socket_path)
|
|
if sock:
|
|
state.connected = True
|
|
# Subscribe to events
|
|
send_request(sock, {"type": "subscribe", "id": request_id})
|
|
request_id += 1
|
|
# Get initial state
|
|
send_request(sock, {"type": "state", "id": request_id})
|
|
request_id += 1
|
|
else:
|
|
state.connected = False
|
|
|
|
# Read events
|
|
if sock:
|
|
events = read_events(sock)
|
|
for event in events:
|
|
process_event(event, state)
|
|
|
|
# Periodic ping
|
|
if time.time() - last_ping > 5:
|
|
send_request(sock, {"type": "ping", "id": request_id})
|
|
request_id += 1
|
|
last_ping = time.time()
|
|
|
|
# Render dashboard
|
|
render_dashboard(state)
|
|
|
|
# Small delay
|
|
time.sleep(0.1)
|
|
|
|
except KeyboardInterrupt:
|
|
print(f"\n{Colors.DIM}Exiting...{Colors.RESET}")
|
|
break
|
|
except BrokenPipeError:
|
|
state.connected = False
|
|
sock = None
|
|
except Exception as e:
|
|
state.errors.append(str(e))
|
|
time.sleep(1)
|
|
|
|
if sock:
|
|
sock.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|