fix(gui): Resolve ImGui stack corruption, JSON serialization errors, and test regressions
This commit is contained in:
@@ -91,7 +91,14 @@ class AsyncEventQueue:
|
|||||||
"""
|
"""
|
||||||
self._queue.put((event_name, payload))
|
self._queue.put((event_name, payload))
|
||||||
if self.websocket_server:
|
if self.websocket_server:
|
||||||
self.websocket_server.broadcast("events", {"event": event_name, "payload": payload})
|
# Ensure payload is JSON serializable for websocket broadcast
|
||||||
|
serializable_payload = payload
|
||||||
|
if hasattr(payload, 'to_dict'):
|
||||||
|
serializable_payload = payload.to_dict()
|
||||||
|
elif hasattr(payload, '__dict__'):
|
||||||
|
serializable_payload = vars(payload)
|
||||||
|
|
||||||
|
self.websocket_server.broadcast("events", {"event": event_name, "payload": serializable_payload})
|
||||||
|
|
||||||
def get(self) -> Tuple[str, Any]:
|
def get(self) -> Tuple[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
46
src/gui_2.py
46
src/gui_2.py
@@ -2253,7 +2253,7 @@ def hello():
|
|||||||
|
|
||||||
def _render_discussion_panel(self) -> None:
|
def _render_discussion_panel(self) -> None:
|
||||||
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel")
|
||||||
# THINKING indicator
|
# THINKING indicator
|
||||||
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...']
|
||||||
if is_thinking:
|
if is_thinking:
|
||||||
val = math.sin(time.time() * 10 * math.pi)
|
val = math.sin(time.time() * 10 * math.pi)
|
||||||
@@ -2262,12 +2262,10 @@ def hello():
|
|||||||
if theme.is_nerv_active():
|
if theme.is_nerv_active():
|
||||||
c = vec4(255, 50, 50, alpha) # More vibrant for NERV
|
c = vec4(255, 50, 50, alpha) # More vibrant for NERV
|
||||||
imgui.text_colored(c, "THINKING...")
|
imgui.text_colored(c, "THINKING...")
|
||||||
imgui.separator()
|
imgui.same_line()
|
||||||
# Prior session viewing mode
|
|
||||||
if self.is_viewing_prior_session:
|
if self.is_viewing_prior_session:
|
||||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20))
|
imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20))
|
||||||
imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION")
|
|
||||||
imgui.same_line()
|
|
||||||
if imgui.button("Exit Prior Session"):
|
if imgui.button("Exit Prior Session"):
|
||||||
self.controller.cb_exit_prior_session()
|
self.controller.cb_exit_prior_session()
|
||||||
self._comms_log_dirty = True
|
self._comms_log_dirty = True
|
||||||
@@ -2289,7 +2287,7 @@ def hello():
|
|||||||
if ts:
|
if ts:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
imgui.text_colored(vec4(160, 160, 160), str(ts))
|
||||||
|
|
||||||
content = entry.get("content", "")
|
content = entry.get("content", "")
|
||||||
if collapsed:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2301,12 +2299,14 @@ def hello():
|
|||||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||||
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
markdown_helper.render(content, context_id=f'prior_disc_{idx}')
|
||||||
if is_nerv: imgui.pop_style_color()
|
if is_nerv: imgui.pop_style_color()
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
imgui.pop_style_color()
|
imgui.pop_style_color()
|
||||||
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open):
|
||||||
names = self._get_discussion_names()
|
names = self._get_discussion_names()
|
||||||
grouped_discussions = {}
|
grouped_discussions = {}
|
||||||
@@ -2335,13 +2335,14 @@ def hello():
|
|||||||
for take_name in current_takes:
|
for take_name in current_takes:
|
||||||
label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title()
|
label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title()
|
||||||
flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0
|
flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0
|
||||||
opened, _ = imgui.begin_tab_item(f"{label}###{take_name}", None, flags)
|
res = imgui.begin_tab_item(f"{label}###{take_name}", None, flags)
|
||||||
if opened:
|
if res[0]:
|
||||||
if take_name != self.active_discussion:
|
if take_name != self.active_discussion:
|
||||||
self._switch_discussion(take_name)
|
self._switch_discussion(take_name)
|
||||||
imgui.end_tab_item()
|
imgui.end_tab_item()
|
||||||
|
|
||||||
if imgui.begin_tab_item("Synthesis###Synthesis"):
|
res_s = imgui.begin_tab_item("Synthesis###Synthesis")
|
||||||
|
if res_s[0]:
|
||||||
self._render_synthesis_panel()
|
self._render_synthesis_panel()
|
||||||
imgui.end_tab_item()
|
imgui.end_tab_item()
|
||||||
|
|
||||||
@@ -2373,10 +2374,13 @@ def hello():
|
|||||||
self._flush_disc_entries_to_project()
|
self._flush_disc_entries_to_project()
|
||||||
# Restore project discussion
|
# Restore project discussion
|
||||||
self._switch_discussion(self.active_discussion)
|
self._switch_discussion(self.active_discussion)
|
||||||
|
self.ai_status = "track discussion disabled"
|
||||||
|
|
||||||
disc_sec = self.project.get("discussion", {})
|
disc_sec = self.project.get("discussion", {})
|
||||||
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
|
disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {})
|
||||||
git_commit = disc_data.get("git_commit", "")
|
git_commit = disc_data.get("git_commit", "")
|
||||||
last_updated = disc_data.get("last_updated", "")
|
last_updated = disc_data.get("last_updated", "")
|
||||||
|
|
||||||
imgui.text_colored(C_LBL, "commit:")
|
imgui.text_colored(C_LBL, "commit:")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
self._render_selectable_label('git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
self._render_selectable_label('git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL))
|
||||||
@@ -2389,9 +2393,11 @@ def hello():
|
|||||||
disc_data["git_commit"] = cmt
|
disc_data["git_commit"] = cmt
|
||||||
disc_data["last_updated"] = project_manager.now_ts()
|
disc_data["last_updated"] = project_manager.now_ts()
|
||||||
self.ai_status = f"commit: {cmt[:12]}"
|
self.ai_status = f"commit: {cmt[:12]}"
|
||||||
|
|
||||||
imgui.text_colored(C_LBL, "updated:")
|
imgui.text_colored(C_LBL, "updated:")
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text_colored(C_SUB, last_updated if last_updated else "(never)")
|
imgui.text_colored(C_SUB, last_updated if last_updated else "(never)")
|
||||||
|
|
||||||
ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input)
|
ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input)
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Create"):
|
if imgui.button("Create"):
|
||||||
@@ -2404,6 +2410,7 @@ def hello():
|
|||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Delete"):
|
if imgui.button("Delete"):
|
||||||
self._delete_discussion(self.active_discussion)
|
self._delete_discussion(self.active_discussion)
|
||||||
|
|
||||||
if not self.is_viewing_prior_session:
|
if not self.is_viewing_prior_session:
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
if imgui.button("+ Entry"):
|
if imgui.button("+ Entry"):
|
||||||
@@ -2423,6 +2430,7 @@ def hello():
|
|||||||
self._flush_to_config()
|
self._flush_to_config()
|
||||||
models.save_config(self.config)
|
models.save_config(self.config)
|
||||||
self.ai_status = "discussion saved"
|
self.ai_status = "discussion saved"
|
||||||
|
|
||||||
ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history)
|
||||||
# Truncation controls
|
# Truncation controls
|
||||||
imgui.text("Keep Pairs:")
|
imgui.text("Keep Pairs:")
|
||||||
@@ -2435,15 +2443,19 @@ def hello():
|
|||||||
with self._disc_entries_lock:
|
with self._disc_entries_lock:
|
||||||
self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs)
|
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"):
|
||||||
imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True)
|
imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True)
|
||||||
for i, r in enumerate(self.disc_roles):
|
for i, r in enumerate(self.disc_roles):
|
||||||
if imgui.button(f"x##r{i}"):
|
imgui.push_id(f"role_{i}")
|
||||||
|
if imgui.button("X"):
|
||||||
self.disc_roles.pop(i)
|
self.disc_roles.pop(i)
|
||||||
|
imgui.pop_id()
|
||||||
break
|
break
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
imgui.text(r)
|
imgui.text(r)
|
||||||
|
imgui.pop_id()
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input)
|
ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input)
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2452,9 +2464,10 @@ def hello():
|
|||||||
if r and r not in self.disc_roles:
|
if r and r not in self.disc_roles:
|
||||||
self.disc_roles.append(r)
|
self.disc_roles.append(r)
|
||||||
self.ui_disc_new_role_input = ""
|
self.ui_disc_new_role_input = ""
|
||||||
|
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False)
|
imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False)
|
||||||
|
|
||||||
# Filter entries based on focused agent persona
|
# Filter entries based on focused agent persona
|
||||||
display_entries = self.disc_entries
|
display_entries = self.disc_entries
|
||||||
if self.ui_focus_agent:
|
if self.ui_focus_agent:
|
||||||
@@ -2485,10 +2498,12 @@ def hello():
|
|||||||
if imgui.selectable(r, r == entry["role"])[0]:
|
if imgui.selectable(r, r == entry["role"])[0]:
|
||||||
entry["role"] = r
|
entry["role"] = r
|
||||||
imgui.end_combo()
|
imgui.end_combo()
|
||||||
|
|
||||||
if not collapsed:
|
if not collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("[Edit]" if read_mode else "[Read]"):
|
if imgui.button("[Edit]" if read_mode else "[Read]"):
|
||||||
entry["read_mode"] = not read_mode
|
entry["read_mode"] = not read_mode
|
||||||
|
|
||||||
ts_str = entry.get("ts", "")
|
ts_str = entry.get("ts", "")
|
||||||
if ts_str:
|
if ts_str:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
@@ -2509,6 +2524,7 @@ def hello():
|
|||||||
if imgui.is_item_hovered():
|
if imgui.is_item_hovered():
|
||||||
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
|
tooltip = "Files injected at this point:\n" + "\n".join([f.path for f in injected_here])
|
||||||
imgui.set_tooltip(tooltip)
|
imgui.set_tooltip(tooltip)
|
||||||
|
|
||||||
if collapsed:
|
if collapsed:
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
if imgui.button("Ins"):
|
if imgui.button("Ins"):
|
||||||
@@ -2522,10 +2538,10 @@ def hello():
|
|||||||
if imgui.button("Branch"):
|
if imgui.button("Branch"):
|
||||||
self._branch_discussion(i)
|
self._branch_discussion(i)
|
||||||
imgui.same_line()
|
imgui.same_line()
|
||||||
preview = entry["content"].replace("\\n", " ")[:60]
|
preview = entry["content"].replace("\n", " ")[:60]
|
||||||
if len(entry["content"]) > 60: preview += "..."
|
if len(entry["content"]) > 60: preview += "..."
|
||||||
if not preview.strip() and entry.get("thinking_segments"):
|
if not preview.strip() and entry.get("thinking_segments"):
|
||||||
preview = entry["thinking_segments"][0]["content"].replace("\\n", " ")[:60]
|
preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60]
|
||||||
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
|
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
|
||||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||||
if not collapsed:
|
if not collapsed:
|
||||||
@@ -2582,11 +2598,13 @@ def hello():
|
|||||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||||
imgui.separator()
|
imgui.separator()
|
||||||
imgui.pop_id()
|
imgui.pop_id()
|
||||||
|
|
||||||
if self._scroll_disc_to_bottom:
|
if self._scroll_disc_to_bottom:
|
||||||
imgui.set_scroll_here_y(1.0)
|
imgui.set_scroll_here_y(1.0)
|
||||||
self._scroll_disc_to_bottom = False
|
self._scroll_disc_to_bottom = False
|
||||||
|
|
||||||
imgui.end_child()
|
imgui.end_child()
|
||||||
|
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel")
|
||||||
|
|
||||||
def _render_synthesis_panel(self) -> None:
|
def _render_synthesis_panel(self) -> None:
|
||||||
"""Renders a panel for synthesizing multiple discussion takes."""
|
"""Renders a panel for synthesizing multiple discussion takes."""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ def test_file_item_fields():
|
|||||||
assert item.path == "src/models.py"
|
assert item.path == "src/models.py"
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
assert item.force_full is False
|
assert item.force_full is False
|
||||||
|
assert item.injected_at is None
|
||||||
|
|
||||||
def test_file_item_to_dict():
|
def test_file_item_to_dict():
|
||||||
"""Test that FileItem can be serialized to a dict."""
|
"""Test that FileItem can be serialized to a dict."""
|
||||||
@@ -14,7 +15,8 @@ def test_file_item_to_dict():
|
|||||||
expected = {
|
expected = {
|
||||||
"path": "test.py",
|
"path": "test.py",
|
||||||
"auto_aggregate": False,
|
"auto_aggregate": False,
|
||||||
"force_full": True
|
"force_full": True,
|
||||||
|
"injected_at": None
|
||||||
}
|
}
|
||||||
assert item.to_dict() == expected
|
assert item.to_dict() == expected
|
||||||
|
|
||||||
@@ -23,12 +25,14 @@ def test_file_item_from_dict():
|
|||||||
data = {
|
data = {
|
||||||
"path": "test.py",
|
"path": "test.py",
|
||||||
"auto_aggregate": False,
|
"auto_aggregate": False,
|
||||||
"force_full": True
|
"force_full": True,
|
||||||
|
"injected_at": 123.456
|
||||||
}
|
}
|
||||||
item = FileItem.from_dict(data)
|
item = FileItem.from_dict(data)
|
||||||
assert item.path == "test.py"
|
assert item.path == "test.py"
|
||||||
assert item.auto_aggregate is False
|
assert item.auto_aggregate is False
|
||||||
assert item.force_full is True
|
assert item.force_full is True
|
||||||
|
assert item.injected_at == 123.456
|
||||||
|
|
||||||
def test_file_item_from_dict_defaults():
|
def test_file_item_from_dict_defaults():
|
||||||
"""Test that FileItem.from_dict handles missing fields."""
|
"""Test that FileItem.from_dict handles missing fields."""
|
||||||
@@ -37,3 +41,4 @@ def test_file_item_from_dict_defaults():
|
|||||||
assert item.path == "test.py"
|
assert item.path == "test.py"
|
||||||
assert item.auto_aggregate is True
|
assert item.auto_aggregate is True
|
||||||
assert item.force_full is False
|
assert item.force_full is False
|
||||||
|
assert item.injected_at is None
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ def test_render_discussion_panel_symbol_lookup(mock_app, role):
|
|||||||
with (
|
with (
|
||||||
patch('src.gui_2.imgui') as mock_imgui,
|
patch('src.gui_2.imgui') as mock_imgui,
|
||||||
patch('src.gui_2.mcp_client') as mock_mcp,
|
patch('src.gui_2.mcp_client') as mock_mcp,
|
||||||
patch('src.gui_2.project_manager') as mock_pm
|
patch('src.gui_2.project_manager') as mock_pm,
|
||||||
|
patch('src.markdown_helper.imgui_md') as mock_md
|
||||||
):
|
):
|
||||||
# Set up App instance state
|
# Set up App instance state
|
||||||
mock_app.perf_profiling_enabled = False
|
mock_app.perf_profiling_enabled = False
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from src.gui_2 import App
|
|||||||
|
|
||||||
|
|
||||||
def _make_app(**kwargs):
|
def _make_app(**kwargs):
|
||||||
app = MagicMock(spec=App)
|
app = MagicMock()
|
||||||
app.mma_streams = kwargs.get("mma_streams", {})
|
app.mma_streams = kwargs.get("mma_streams", {})
|
||||||
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
app.mma_tier_usage = kwargs.get("mma_tier_usage", {
|
||||||
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
"Tier 1": {"input": 0, "output": 0, "model": "gemini-3.1-pro-preview"},
|
||||||
@@ -13,6 +13,7 @@ def _make_app(**kwargs):
|
|||||||
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 3": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||||
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
"Tier 4": {"input": 0, "output": 0, "model": "gemini-2.5-flash-lite"},
|
||||||
})
|
})
|
||||||
|
app.ui_focus_agent = kwargs.get("ui_focus_agent", None)
|
||||||
app.tracks = kwargs.get("tracks", [])
|
app.tracks = kwargs.get("tracks", [])
|
||||||
app.active_track = kwargs.get("active_track", None)
|
app.active_track = kwargs.get("active_track", None)
|
||||||
app.active_tickets = kwargs.get("active_tickets", [])
|
app.active_tickets = kwargs.get("active_tickets", [])
|
||||||
|
|||||||
Reference in New Issue
Block a user