fix(gui): move lock init before use, protect disc_entries with threading lock

This commit is contained in:
2026-03-02 10:15:20 -05:00
parent c14150fa81
commit fc9634fd73
2 changed files with 181 additions and 113 deletions

293
gui_2.py
View File

@@ -161,6 +161,16 @@ class App:
"""The main ImGui interface orchestrator for Manual Slop.""" """The main ImGui interface orchestrator for Manual Slop."""
def __init__(self) -> None: def __init__(self) -> None:
# Initialize locks first to avoid initialization order issues
self._send_thread_lock = threading.Lock()
self._disc_entries_lock = threading.Lock()
self._pending_comms_lock = threading.Lock()
self._pending_tool_calls_lock = threading.Lock()
self._pending_history_adds_lock = threading.Lock()
self._pending_gui_tasks_lock = threading.Lock()
self._pending_dialog_lock = threading.Lock()
self._api_event_queue_lock = threading.Lock()
self.config = load_config() self.config = load_config()
self.event_queue = events.AsyncEventQueue() self.event_queue = events.AsyncEventQueue()
self._loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
@@ -185,7 +195,8 @@ class App:
self.disc_roles: list[str] = list(disc_sec.get("roles", list(DISC_ROLES))) self.disc_roles: list[str] = list(disc_sec.get("roles", list(DISC_ROLES)))
self.active_discussion = disc_sec.get("active", "main") self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {}) disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
self.disc_entries: list[dict[str, Any]] = _parse_history_entries(disc_data.get("history", []), self.disc_roles) with self._disc_entries_lock:
self.disc_entries: list[dict[str, Any]] = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.ui_output_dir = self.project.get("output", {}).get("output_dir", "./md_gen") self.ui_output_dir = self.project.get("output", {}).get("output_dir", "./md_gen")
self.ui_files_base_dir = self.project.get("files", {}).get("base_dir", ".") self.ui_files_base_dir = self.project.get("files", {}).get("base_dir", ".")
self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".") self.ui_shots_base_dir = self.project.get("screenshots", {}).get("base_dir", ".")
@@ -216,7 +227,6 @@ class App:
self.last_md_path: Path | None = None self.last_md_path: Path | None = None
self.last_file_items: list[Any] = [] self.last_file_items: list[Any] = []
self.send_thread: threading.Thread | None = None self.send_thread: threading.Thread | None = None
self._send_thread_lock = threading.Lock()
self.models_thread: threading.Thread | None = None self.models_thread: threading.Thread | None = None
_default_windows = { _default_windows = {
"Context Hub": True, "Context Hub": True,
@@ -241,7 +251,6 @@ class App:
self.text_viewer_content = "" self.text_viewer_content = ""
self._pending_dialog: ConfirmDialog | None = None self._pending_dialog: ConfirmDialog | None = None
self._pending_dialog_open = False self._pending_dialog_open = False
self._pending_dialog_lock = threading.Lock()
self._pending_actions: dict[str, ConfirmDialog] = {} self._pending_actions: dict[str, ConfirmDialog] = {}
self._pending_ask_dialog = False self._pending_ask_dialog = False
self._ask_dialog_open = False self._ask_dialog_open = False
@@ -270,11 +279,8 @@ class App:
self._tool_log: list[tuple[str, str, float]] = [] self._tool_log: list[tuple[str, str, float]] = []
self._comms_log: list[dict[str, Any]] = [] self._comms_log: list[dict[str, Any]] = []
self._pending_comms: list[dict[str, Any]] = [] self._pending_comms: list[dict[str, Any]] = []
self._pending_comms_lock = threading.Lock()
self._pending_tool_calls: list[tuple[str, str, float]] = [] self._pending_tool_calls: list[tuple[str, str, float]] = []
self._pending_tool_calls_lock = threading.Lock()
self._pending_history_adds: list[dict[str, Any]] = [] self._pending_history_adds: list[dict[str, Any]] = []
self._pending_history_adds_lock = threading.Lock()
self._trigger_blink = False self._trigger_blink = False
self._is_blinking = False self._is_blinking = False
self._blink_start_time = 0.0 self._blink_start_time = 0.0
@@ -285,7 +291,6 @@ class App:
self._scroll_comms_to_bottom = False self._scroll_comms_to_bottom = False
self._scroll_tool_calls_to_bottom = False self._scroll_tool_calls_to_bottom = False
self._pending_gui_tasks: list[dict[str, Any]] = [] self._pending_gui_tasks: list[dict[str, Any]] = []
self._pending_gui_tasks_lock = threading.Lock()
self.session_usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0} self.session_usage = {"input_tokens": 0, "output_tokens": 0, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0}
self._token_budget_pct = 0.0 self._token_budget_pct = 0.0
self._token_budget_current = 0 self._token_budget_current = 0
@@ -427,6 +432,8 @@ class App:
} }
self._discussion_names_cache: list[str] = [] self._discussion_names_cache: list[str] = []
self._discussion_names_dirty: bool = True self._discussion_names_dirty: bool = True
self.hook_server = api_hooks.HookServer(self)
self.hook_server.start()
def create_api(self) -> FastAPI: def create_api(self) -> FastAPI:
"""Creates and configures the FastAPI application for headless mode.""" """Creates and configures the FastAPI application for headless mode."""
@@ -669,7 +676,8 @@ class App:
self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES))) self.disc_roles = list(disc_sec.get("roles", list(DISC_ROLES)))
self.active_discussion = disc_sec.get("active", "main") self.active_discussion = disc_sec.get("active", "main")
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {}) disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
self.disc_entries = _parse_history_entries(disc_data.get("history", []), self.disc_roles) with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
proj = self.project proj = self.project
self.ui_output_dir = proj.get("output", {}).get("output_dir", "./md_gen") self.ui_output_dir = proj.get("output", {}).get("output_dir", "./md_gen")
self.ui_files_base_dir = proj.get("files", {}).get("base_dir", ".") self.ui_files_base_dir = proj.get("files", {}).get("base_dir", ".")
@@ -712,7 +720,8 @@ class App:
if self.active_track: if self.active_track:
track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir) track_history = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
if track_history: if track_history:
self.disc_entries = _parse_history_entries(track_history, self.disc_roles) with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(track_history, self.disc_roles)
def _cb_load_track(self, track_id: str) -> None: def _cb_load_track(self, track_id: str) -> None:
state = project_manager.load_track_state(track_id, self.ui_files_base_dir) state = project_manager.load_track_state(track_id, self.ui_files_base_dir)
@@ -735,10 +744,11 @@ class App:
self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in tickets] self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in tickets]
# Load track-scoped history # Load track-scoped history
history = project_manager.load_track_history(track_id, self.ui_files_base_dir) history = project_manager.load_track_history(track_id, self.ui_files_base_dir)
if history: with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(history, self.disc_roles) if history:
else: self.disc_entries = _parse_history_entries(history, self.disc_roles)
self.disc_entries = [] else:
self.disc_entries = []
self._recalculate_session_usage() self._recalculate_session_usage()
self.ai_status = f"Loaded track: {state.metadata.name}" self.ai_status = f"Loaded track: {state.metadata.name}"
except Exception as e: except Exception as e:
@@ -769,12 +779,20 @@ class App:
self.ai_status = f"discussion not found: {name}" self.ai_status = f"discussion not found: {name}"
return return
self.active_discussion = name self.active_discussion = name
self.active_discussion_idx = -1
discussions_root = self.project.get("discussions", [])
for i, d in enumerate(discussions_root):
if isinstance(d, dict) and d.get("title") == name:
self.active_discussion_idx = i
break
self._track_discussion_active = False self._track_discussion_active = False
disc_sec["active"] = name disc_sec["active"] = name
self._discussion_names_dirty = True self._discussion_names_dirty = True
disc_data = discussions[name] disc_data = discussions[name]
self.disc_entries = _parse_history_entries(disc_data.get("history", []), self.disc_roles) with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(disc_data.get("history", []), self.disc_roles)
self.ai_status = f"discussion: {name}" self.ai_status = f"discussion: {name}"
sys.stderr.write(f'[DEBUG] Switched to {name}. disc_entries len: {len(self.disc_entries)}\n')
def _flush_disc_entries_to_project(self) -> None: def _flush_disc_entries_to_project(self) -> None:
history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries] history_strings = [project_manager.entry_to_str(e) for e in self.disc_entries]
@@ -827,10 +845,29 @@ class App:
# ---------------------------------------------------------------- logic # ---------------------------------------------------------------- logic
def _on_comms_entry(self, entry: dict) -> None: def _on_comms_entry(self, entry: dict) -> None:
# sys.stderr.write(f"[DEBUG] _on_comms_entry: {entry.get('kind')} {entry.get('direction')}\n")
session_logger.log_comms(entry) session_logger.log_comms(entry)
entry["local_ts"] = time.time() entry["local_ts"] = time.time()
kind = entry.get("kind")
payload = entry.get("payload", {})
if kind in ("tool_result", "tool_call"):
role = "Tool" if kind == "tool_result" else "Vendor API"
content = ""
if kind == "tool_result":
content = payload.get("output", "")
else:
content = payload.get("script") or payload.get("args") or payload.get("message", "")
if isinstance(content, dict):
content = json.dumps(content, indent=1)
with self._pending_history_adds_lock:
self._pending_history_adds.append({
"role": role,
"content": f"[{kind.upper().replace('_', ' ')}]\n{content}",
"collapsed": True,
"ts": entry.get("ts", project_manager.now_ts())
})
# If this is a history_add kind, route it to history queue instead # If this is a history_add kind, route it to history queue instead
if entry.get("kind") == "history_add": if kind == "history_add":
payload = entry.get("payload", {}) payload = entry.get("payload", {})
with self._pending_history_adds_lock: with self._pending_history_adds_lock:
self._pending_history_adds.append({ self._pending_history_adds.append({
@@ -1008,6 +1045,32 @@ class App:
except Exception as e: except Exception as e:
print(f"Error executing GUI task: {e}") print(f"Error executing GUI task: {e}")
def _process_pending_history_adds(self) -> None:
"""Synchronizes pending history entries to the active discussion and project state."""
with self._pending_history_adds_lock:
items = self._pending_history_adds[:]
self._pending_history_adds.clear()
if not items:
return
self._scroll_disc_to_bottom = True
for item in items:
role = item.get("role", "unknown")
if item.get("role") and item["role"] not in self.disc_roles:
self.disc_roles.append(item["role"])
disc_sec = self.project.get("discussion", {})
discussions = disc_sec.get("discussions", {})
disc_data = discussions.get(self.active_discussion)
if disc_data is not None:
if item.get("disc_title", self.active_discussion) == self.active_discussion:
if self.disc_entries is not disc_data.get("history"):
if "history" not in disc_data:
disc_data["history"] = []
disc_data["history"].append(project_manager.entry_to_str(item))
disc_data["last_updated"] = project_manager.now_ts()
with self._disc_entries_lock:
self.disc_entries.append(item)
print(f'[DEBUG] Added to disc_entries. Current len: {len(self.disc_entries)}')
def _handle_approve_script(self) -> None: def _handle_approve_script(self) -> None:
"""Logic for approving a pending script via API hooks.""" """Logic for approving a pending script via API hooks."""
print("[DEBUG] _handle_approve_script called") print("[DEBUG] _handle_approve_script called")
@@ -1142,6 +1205,8 @@ class App:
self.ai_status = "session reset" self.ai_status = "session reset"
self.ai_response = "" self.ai_response = ""
self.ui_ai_input = "" self.ui_ai_input = ""
with self._pending_history_adds_lock:
self._pending_history_adds.clear()
def _handle_md_only(self) -> None: def _handle_md_only(self) -> None:
"""Logic for the 'MD Only' action.""" """Logic for the 'MD Only' action."""
@@ -1186,6 +1251,17 @@ class App:
"""Runs the internal asyncio event loop.""" """Runs the internal asyncio event loop."""
asyncio.set_event_loop(self._loop) asyncio.set_event_loop(self._loop)
self._loop.create_task(self._process_event_queue()) self._loop.create_task(self._process_event_queue())
# Fallback: process queues even if GUI thread is idling/stuck
async def queue_fallback():
while True:
try:
self._process_pending_gui_tasks()
self._process_pending_history_adds()
except: pass
await asyncio.sleep(0.1)
self._loop.create_task(queue_fallback())
self._loop.run_forever() self._loop.run_forever()
def shutdown(self) -> None: def shutdown(self) -> None:
@@ -1205,8 +1281,8 @@ class App:
while True: while True:
event_name, payload = await self.event_queue.get() event_name, payload = await self.event_queue.get()
if event_name == "user_request": if event_name == "user_request":
# Handle the request (simulating what was previously in do_send thread) # Handle the request in a separate thread to avoid blocking the loop
self._handle_request_event(payload) self._loop.run_in_executor(None, self._handle_request_event, payload)
elif event_name == "response": elif event_name == "response":
# Handle AI response event # Handle AI response event
with self._pending_gui_tasks_lock: with self._pending_gui_tasks_lock:
@@ -1512,9 +1588,9 @@ class App:
imgui.text(content) imgui.text(content)
imgui.pop_text_wrap_pos() imgui.pop_text_wrap_pos()
else: else:
if imgui.begin_child(f"heavy_text_child_{label}", imgui.ImVec2(0, 80), True): imgui.begin_child(f"heavy_text_child_{label}", imgui.ImVec2(0, 80), True)
imgui.input_text_multiline(f"##{label}_input", content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) imgui.input_text_multiline(f"##{label}_input", content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
imgui.end_child() imgui.end_child()
else: else:
if self.ui_word_wrap: if self.ui_word_wrap:
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
@@ -1583,15 +1659,6 @@ class App:
for tc in self._pending_tool_calls: for tc in self._pending_tool_calls:
self._tool_log.append(tc) self._tool_log.append(tc)
self._pending_tool_calls.clear() self._pending_tool_calls.clear()
# Sync pending history adds
with self._pending_history_adds_lock:
if self._pending_history_adds:
self._scroll_disc_to_bottom = True
for item in self._pending_history_adds:
if item["role"] not in self.disc_roles:
self.disc_roles.append(item["role"])
self.disc_entries.append(item)
self._pending_history_adds.clear()
# ---- Menubar # ---- Menubar
if imgui.begin_main_menu_bar(): if imgui.begin_main_menu_bar():
if imgui.begin_menu("manual slop"): if imgui.begin_menu("manual slop"):
@@ -1668,9 +1735,9 @@ class App:
exp, self.show_windows["Discussion Hub"] = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"]) exp, self.show_windows["Discussion Hub"] = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"])
if exp: if exp:
# Top part for the history # Top part for the history
if imgui.begin_child("HistoryChild", size=(0, -200)): imgui.begin_child("HistoryChild", size=(0, -200))
self._render_discussion_panel() self._render_discussion_panel()
imgui.end_child() imgui.end_child()
# Bottom part with tabs for message and response # Bottom part with tabs for message and response
if imgui.begin_tab_bar("MessageResponseTabs"): if imgui.begin_tab_bar("MessageResponseTabs"):
if imgui.begin_tab_item("Message")[0]: if imgui.begin_tab_item("Message")[0]:
@@ -2455,7 +2522,8 @@ class App:
if self._track_discussion_active: if self._track_discussion_active:
self._flush_disc_entries_to_project() self._flush_disc_entries_to_project()
history_strings = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir) history_strings = project_manager.load_track_history(self.active_track.id, self.ui_files_base_dir)
self.disc_entries = _parse_history_entries(history_strings, self.disc_roles) with self._disc_entries_lock:
self.disc_entries = _parse_history_entries(history_strings, self.disc_roles)
self.ai_status = f"track discussion: {self.active_track.id}" self.ai_status = f"track discussion: {self.active_track.id}"
else: else:
self._flush_disc_entries_to_project() self._flush_disc_entries_to_project()
@@ -2521,7 +2589,8 @@ class App:
if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1 if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1
imgui.same_line() imgui.same_line()
if imgui.button("Truncate"): if imgui.button("Truncate"):
self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs) with self._disc_entries_lock:
self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs)
self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs" self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs"
imgui.separator() imgui.separator()
if imgui.collapsing_header("Roles"): if imgui.collapsing_header("Roles"):
@@ -2831,51 +2900,51 @@ class App:
if imgui.button("Clear##tc"): if imgui.button("Clear##tc"):
self._tool_log.clear() self._tool_log.clear()
imgui.separator() imgui.separator()
if imgui.begin_child("tc_scroll"): imgui.begin_child("scroll_area")
clipper = imgui.ListClipper() clipper = imgui.ListClipper()
clipper.begin(len(self._tool_log)) clipper.begin(len(self._tool_log))
while clipper.step(): while clipper.step():
for i_minus_one in range(clipper.display_start, clipper.display_end): for i_minus_one in range(clipper.display_start, clipper.display_end):
i = i_minus_one + 1 i = i_minus_one + 1
script, result, _ = self._tool_log[i_minus_one] script, result, _ = self._tool_log[i_minus_one]
first_line = script.strip().splitlines()[0][:80] if script.strip() else "(empty)" first_line = script.strip().splitlines()[0][:80] if script.strip() else "(empty)"
imgui.text_colored(C_KEY, f"Call #{i}: {first_line}") imgui.text_colored(C_KEY, f"Call #{i}: {first_line}")
# Script Display # Script Display
imgui.text_colored(C_LBL, "Script:") imgui.text_colored(C_LBL, "Script:")
imgui.same_line() imgui.same_line()
if imgui.button(f"[+]##script_{i}"): if imgui.button(f"[+]##script_{i}"):
self.show_text_viewer = True self.show_text_viewer = True
self.text_viewer_title = f"Call Script #{i}" self.text_viewer_title = f"Call Script #{i}"
self.text_viewer_content = script self.text_viewer_content = script
if self.ui_word_wrap: if self.ui_word_wrap:
if imgui.begin_child(f"tc_script_wrap_{i}", imgui.ImVec2(-1, 72), True): imgui.begin_child(f"tc_script_wrap_{i}", imgui.ImVec2(-1, 72), True)
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text(script) imgui.text(script)
imgui.pop_text_wrap_pos() imgui.pop_text_wrap_pos()
imgui.end_child() imgui.end_child()
else: else:
if imgui.begin_child(f"tc_script_fixed_width_{i}", imgui.ImVec2(0, 72), True, imgui.WindowFlags_.horizontal_scrollbar): imgui.begin_child(f"tc_script_fixed_width_{i}", imgui.ImVec2(0, 72), True, imgui.WindowFlags_.horizontal_scrollbar)
imgui.input_text_multiline(f"##tc_script_res_{i}", script, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) imgui.input_text_multiline(f"##tc_script_res_{i}", script, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
imgui.end_child() imgui.end_child()
# Result Display # Result Display
imgui.text_colored(C_LBL, "Output:") imgui.text_colored(C_LBL, "Output:")
imgui.same_line() imgui.same_line()
if imgui.button(f"[+]##output_{i}"): if imgui.button(f"[+]##output_{i}"):
self.show_text_viewer = True self.show_text_viewer = True
self.text_viewer_title = f"Call Output #{i}" self.text_viewer_title = f"Call Output #{i}"
self.text_viewer_content = result self.text_viewer_content = result
if self.ui_word_wrap: if self.ui_word_wrap:
if imgui.begin_child(f"tc_res_wrap_{i}", imgui.ImVec2(-1, 72), True): imgui.begin_child(f"tc_res_wrap_{i}", imgui.ImVec2(-1, 72), True)
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
imgui.text(result) imgui.text(result)
imgui.pop_text_wrap_pos() imgui.pop_text_wrap_pos()
imgui.end_child() imgui.end_child()
else: else:
if imgui.begin_child(f"tc_res_fixed_width_{i}", imgui.ImVec2(0, 72), True, imgui.WindowFlags_.horizontal_scrollbar): imgui.begin_child(f"tc_res_fixed_width_{i}", imgui.ImVec2(0, 72), True, imgui.WindowFlags_.horizontal_scrollbar)
imgui.input_text_multiline(f"##tc_res_val_{i}", result, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) imgui.input_text_multiline(f"##tc_res_val_{i}", result, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
imgui.end_child() imgui.end_child()
imgui.separator() imgui.separator()
imgui.end_child() imgui.end_child()
def _render_comms_history_panel(self) -> None: def _render_comms_history_panel(self) -> None:
imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}") imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}")
@@ -2895,21 +2964,21 @@ class App:
imgui.separator() imgui.separator()
imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION") imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION")
imgui.separator() imgui.separator()
if imgui.begin_child("comms_scroll"): imgui.begin_child("scroll_area")
clipper = imgui.ListClipper() clipper = imgui.ListClipper()
clipper.begin(len(self._comms_log)) clipper.begin(len(self._comms_log))
while clipper.step(): while clipper.step():
for i in range(clipper.display_start, clipper.display_end): for i in range(clipper.display_start, clipper.display_end):
entry = self._comms_log[i] entry = self._comms_log[i]
imgui.text_colored(C_KEY, f"[{entry.get('direction')}] {entry.get('type')}") imgui.text_colored(C_KEY, f"[{entry.get('direction')}] {entry.get('type')}")
imgui.same_line() imgui.same_line()
if imgui.button(f"[+]##c{i}"): if imgui.button(f"[+]##c{i}"):
self.show_text_viewer = True self.show_text_viewer = True
self.text_viewer_title = f"Comms Entry #{i}" self.text_viewer_title = f"Comms Entry #{i}"
self.text_viewer_content = json.dumps(entry.get("payload"), indent=2) self.text_viewer_content = json.dumps(entry.get("payload"), indent=2)
imgui.text_unformatted(str(entry.get("payload"))[:200] + "...") imgui.text_unformatted(str(entry.get("payload"))[:200] + "...")
imgui.separator() imgui.separator()
imgui.end_child() imgui.end_child()
def _render_mma_dashboard(self) -> None: def _render_mma_dashboard(self) -> None:
# Task 5.3: Dense Summary Line # Task 5.3: Dense Summary Line
@@ -3172,15 +3241,15 @@ class App:
def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None: def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None:
if stream_key is not None: if stream_key is not None:
content = self.mma_streams.get(stream_key, "") content = self.mma_streams.get(stream_key, "")
if imgui.begin_child("##stream_content", imgui.ImVec2(-1, -1)): imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1))
imgui.text_wrapped(content) imgui.text_wrapped(content)
try: try:
if len(content) != self._tier_stream_last_len.get(stream_key, -1): if len(content) != self._tier_stream_last_len.get(stream_key, -1):
imgui.set_scroll_here_y(1.0) imgui.set_scroll_here_y(1.0)
self._tier_stream_last_len[stream_key] = len(content) self._tier_stream_last_len[stream_key] = len(content)
except (TypeError, AttributeError): except (TypeError, AttributeError):
pass pass
imgui.end_child() imgui.end_child()
else: else:
tier3_keys = [k for k in self.mma_streams if "Tier 3" in k] tier3_keys = [k for k in self.mma_streams if "Tier 3" in k]
if not tier3_keys: if not tier3_keys:
@@ -3189,15 +3258,15 @@ class App:
for key in tier3_keys: for key in tier3_keys:
ticket_id = key.split(": ", 1)[-1] if ": " in key else key ticket_id = key.split(": ", 1)[-1] if ": " in key else key
imgui.text(ticket_id) imgui.text(ticket_id)
if imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True): imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True)
imgui.text_wrapped(self.mma_streams[key]) imgui.text_wrapped(self.mma_streams[key])
try: try:
if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1): if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1):
imgui.set_scroll_here_y(1.0) imgui.set_scroll_here_y(1.0)
self._tier_stream_last_len[key] = len(self.mma_streams[key]) self._tier_stream_last_len[key] = len(self.mma_streams[key])
except (TypeError, AttributeError): except (TypeError, AttributeError):
pass pass
imgui.end_child() imgui.end_child()
def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None: def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None:
tid = ticket.get('id', '??') tid = ticket.get('id', '??')

View File

@@ -1 +0,0 @@
Write-Host "Simulation Test"