feat(gui): Implement on-demand definition lookup with clickable navigation and collapsing

This commit is contained in:
2026-03-07 15:20:39 -05:00
parent 0c2df6c188
commit 7ea833e2d3
8 changed files with 245 additions and 17 deletions

View File

@@ -0,0 +1,79 @@
import pytest
from unittest.mock import MagicMock, patch
from src.gui_2 import App
@pytest.mark.parametrize("role", ["User", "AI"])
def test_render_discussion_panel_symbol_lookup(mock_app, role):
# Mock imgui, mcp_client, and project_manager as requested
with (
patch('src.gui_2.imgui') as mock_imgui,
patch('src.gui_2.mcp_client') as mock_mcp,
patch('src.gui_2.project_manager') as mock_pm
):
# Set up App instance state
mock_app.perf_profiling_enabled = False
mock_app.ai_status = "idle"
mock_app.is_viewing_prior_session = False
mock_app.active_discussion = "Default"
mock_app.project = {"discussion": {"discussions": {"Default": {}}}}
mock_app.disc_entries = [{"role": role, "content": "[Definition: MyClass from src/models.py (line 10)]", "collapsed": False, "read_mode": True}]
mock_app.disc_roles = ["User", "AI"]
mock_app.ui_files_base_dir = "."
mock_app.active_track = None
mock_app.ui_disc_new_name_input = ""
mock_app.ui_auto_add_history = False
mock_app.ui_disc_truncate_pairs = 10
mock_app.ui_disc_new_role_input = ""
mock_app._disc_entries_lock = MagicMock()
mock_app._scroll_disc_to_bottom = False
mock_app.ui_word_wrap = False
mock_app.show_text_viewer = False
mock_app.text_viewer_title = ""
mock_app.text_viewer_content = ""
# Mock internal methods to avoid side effects
mock_app._get_discussion_names = MagicMock(return_value=["Default"])
mock_app._render_text_viewer = MagicMock()
# Mock imgui behavior to reach the entry rendering loop
mock_imgui.collapsing_header.return_value = True
mock_imgui.begin_combo.return_value = False
mock_imgui.begin_child.return_value = True
mock_imgui.checkbox.return_value = (False, False)
mock_imgui.input_text.side_effect = lambda label, value, *args, **kwargs: (False, value)
mock_imgui.input_text_multiline.side_effect = lambda label, value, *args, **kwargs: (False, value)
mock_imgui.input_int.side_effect = lambda label, value, *args, **kwargs: (False, value)
# Mock clipper to process the single entry
mock_clipper = MagicMock()
mock_imgui.ListClipper.return_value = mock_clipper
mock_clipper.step.side_effect = [True, False]
mock_clipper.display_start = 0
mock_clipper.display_end = 1
# Mock button click for the [Source] button
# The code renders: if imgui.button(f"[Source]##{i}_{match.start()}"):
# We want it to return True for our entry at index 0.
def button_side_effect(label):
if label == "[Source]##0_0":
return True
return False
mock_imgui.button.side_effect = button_side_effect
# Mock mcp_client.read_file return value
mock_mcp.read_file.return_value = "class MyClass:\n pass"
# Execute the panel rendering
mock_app._render_discussion_panel()
# Assertions
# 1. Assert that the regex correctly identifies the pattern and imgui.button('[Source]##0_0') is called
mock_imgui.button.assert_any_call("[Source]##0_0")
# 2. Verify mcp_client.read_file('src/models.py') is called upon button click
mock_mcp.read_file.assert_called_with("src/models.py")
# 3. Verify the text viewer state is updated correctly
assert mock_app.text_viewer_title == "src/models.py"
assert mock_app.text_viewer_content == "class MyClass:\n pass"
assert mock_app.show_text_viewer is True

View File

@@ -32,24 +32,25 @@ class TestSymbolLookup(unittest.TestCase):
files = ["file1.py", "file2.py"]
symbol = "my_func"
def_content = "def my_func():\n pass"
line_number = 10
with patch("src.mcp_client.py_get_definition") as mock_get_def:
with patch("src.mcp_client.py_get_symbol_info") as mock_get_info:
# First file not found, second file found
mock_get_def.side_effect = [
mock_get_info.side_effect = [
"ERROR: definition 'my_func' not found in file1.py",
def_content
(def_content, line_number)
]
result = get_symbol_definition(symbol, files)
self.assertEqual(result, ("file2.py", def_content))
self.assertEqual(mock_get_def.call_count, 2)
self.assertEqual(result, ("file2.py", def_content, line_number))
self.assertEqual(mock_get_info.call_count, 2)
def test_get_symbol_definition_not_found(self):
files = ["file1.py"]
symbol = "my_func"
with patch("src.mcp_client.py_get_definition") as mock_get_def:
mock_get_def.return_value = "ERROR: definition 'my_func' not found in file1.py"
with patch("src.mcp_client.py_get_symbol_info") as mock_get_info:
mock_get_info.return_value = "ERROR: definition 'my_func' not found in file1.py"
result = get_symbol_definition(symbol, files)
self.assertIsNone(result)

View File

@@ -0,0 +1,84 @@
import pytest
from unittest.mock import MagicMock, patch
from pathlib import Path
from src.app_controller import AppController
from src.events import UserRequestEvent
@pytest.fixture
def controller():
with (
patch('src.models.load_config', return_value={
"ai": {"provider": "gemini", "model": "model-1"},
"projects": {"paths": [], "active": ""},
"gui": {"show_windows": {}}
}),
patch('src.project_manager.load_project', return_value={}),
patch('src.project_manager.migrate_from_legacy_config', return_value={}),
patch('src.project_manager.save_project'),
patch('src.session_logger.open_session'),
patch('src.app_controller.AppController._init_ai_and_hooks'),
patch('src.app_controller.AppController._fetch_models')
):
c = AppController()
# Mock necessary state
c.ui_files_base_dir = "."
c.event_queue = MagicMock()
return c
def test_handle_generate_send_appends_definitions(controller):
# Setup
file_items = [{"path": "src/models.py", "entry": "src/models.py"}]
controller._do_generate = MagicMock(return_value=(
"full_md", Path("output.md"), file_items, "stable_md", "disc_text"
))
controller.ui_ai_input = "Explain @Track object"
# Mock symbol helpers
with (
patch('src.app_controller.parse_symbols', return_value=["Track"]) as mock_parse,
patch('src.app_controller.get_symbol_definition', return_value=("src/models.py", "class Track: pass")) as mock_get_def,
patch('threading.Thread') as mock_thread
):
# Execute
controller._handle_generate_send()
# Run worker manually
worker = mock_thread.call_args[1]['target']
worker()
# Verify
mock_parse.assert_called_once_with("Explain @Track object")
mock_get_def.assert_called_once()
controller.event_queue.put.assert_called_once()
event_name, event_payload = controller.event_queue.put.call_args[0]
assert event_name == "user_request"
assert isinstance(event_payload, UserRequestEvent)
# Check if definition was appended
expected_suffix = "\n\n[Definition: Track from src/models.py]\n```python\nclass Track: pass\n```"
assert event_payload.prompt == "Explain @Track object" + expected_suffix
def test_handle_generate_send_no_symbols(controller):
# Setup
file_items = [{"path": "src/models.py", "entry": "src/models.py"}]
controller._do_generate = MagicMock(return_value=(
"full_md", Path("output.md"), file_items, "stable_md", "disc_text"
))
controller.ui_ai_input = "Just a normal prompt"
with (
patch('src.app_controller.parse_symbols', return_value=[]) as mock_parse,
patch('threading.Thread') as mock_thread
):
# Execute
controller._handle_generate_send()
# Run worker manually
worker = mock_thread.call_args[1]['target']
worker()
# Verify
controller.event_queue.put.assert_called_once()
_, event_payload = controller.event_queue.put.call_args[0]
assert event_payload.prompt == "Just a normal prompt"