diff --git a/shared/godot/autoloads/event_bus.gd b/shared/godot/autoloads/event_bus.gd index fef23bd..3e2349c 100644 --- a/shared/godot/autoloads/event_bus.gd +++ b/shared/godot/autoloads/event_bus.gd @@ -54,6 +54,12 @@ signal chat_opened # Context menu (right-click on avatar) signal context_menu_opened(position: Vector2i) +# Conversation management +signal conversation_new_requested +signal conversation_switch_requested(conversation_id: String) +signal conversation_changed(conversation_id: String) +signal conversation_deleted(conversation_id: String) + # Window interaction lifecycle (used by avatar_hitbox to pause passthrough) signal drag_started signal drag_ended diff --git a/shared/godot/autoloads/flight_recorder.gd b/shared/godot/autoloads/flight_recorder.gd index 3a2f467..a012079 100644 --- a/shared/godot/autoloads/flight_recorder.gd +++ b/shared/godot/autoloads/flight_recorder.gd @@ -1,11 +1,34 @@ extends Node ## Structured event recorder matching @lilith/flight-recorder schema. ## Reads DEBUG flag from ../.env (relative to res://). -## When DEBUG=TRUE: streams JSON entries to stdout and appends to ../logs/flight_recorder.jsonl. +## When DEBUG=TRUE: streams colored human-readable lines to stdout +## and appends JSON entries to ../logs/flight_recorder.jsonl. ## ## Schema: {"date","time","source","type","content","metadata"} ## Usage: FlightRecorder.record("system.event", "Human description", {"key": value}) +const RESET := "\u001b[0m" +const DIM := "\u001b[90m" +const RED := "\u001b[31m" +const GREEN := "\u001b[32m" +const YELLOW := "\u001b[33m" +const BLUE := "\u001b[34m" +const MAGENTA := "\u001b[35m" +const CYAN := "\u001b[36m" +const WHITE := "\u001b[37m" + +const TYPE_PAD := 24 + +const PREFIX_COLORS := { + "llm": CYAN, + "stt": GREEN, + "tts": MAGENTA, + "companion": YELLOW, + "config": BLUE, + "app_state": BLUE, + "flight_recorder": DIM, +} + var _debug: bool = false var _record_file: FileAccess var _log_path: String @@ -30,21 +53,49 @@ func record(type: String, content: String, metadata: Dictionary = {}) -> void: return var dt := Time.get_datetime_dict_from_system() var ms := Time.get_ticks_msec() % 1000 + var time_str := "%02d:%02d:%02d.%03d" % [dt.hour, dt.minute, dt.second, ms] var entry := { "date": "%04d-%02d-%02d" % [dt.year, dt.month, dt.day], - "time": "%02d:%02d:%02d.%03d" % [dt.hour, dt.minute, dt.second, ms], + "time": time_str, "source": "godot", "type": type, "content": content, "metadata": metadata if not metadata.is_empty() else null, } - var line := JSON.stringify(entry) - print("[FLIGHT] " + line) + var json_line := JSON.stringify(entry) + print(_format_colored(time_str, type, content, entry.metadata)) if _record_file != null: - _record_file.store_line(line) + _record_file.store_line(json_line) _record_file.flush() +func _format_colored(time_str: String, type: String, content: String, metadata: Variant) -> String: + var color := _color_for_type(type) + var padded_type := type + if padded_type.length() < TYPE_PAD: + padded_type += " ".repeat(TYPE_PAD - padded_type.length()) + var meta_str := _format_metadata(metadata) + return DIM + time_str + RESET + " " + color + padded_type + RESET + " " + content + meta_str + + +func _color_for_type(type: String) -> String: + if type.ends_with(".error"): + return RED + var prefix := type.split(".")[0] + return PREFIX_COLORS.get(prefix, WHITE) + + +func _format_metadata(metadata: Variant) -> String: + if metadata == null: + return "" + if metadata is Dictionary and metadata.is_empty(): + return "" + var parts: PackedStringArray = [] + for key: String in metadata: + parts.append("%s=%s" % [key, str(metadata[key])]) + return " " + DIM + "(" + ", ".join(parts) + ")" + RESET + + func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE and _record_file != null: _record_file.close()