chore: import upstream snapshot with attribution
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
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
This commit is contained in:
Executable
+341
@@ -0,0 +1,341 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user