Progress on context composition
This commit is contained in:
+1
-1
@@ -50,7 +50,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
*Link: [./tracks/context_comp_slices_20260510/](./tracks/context_comp_slices_20260510/)*
|
||||
*Goal: Enhance slice visualization with visual editor, annotation support (tags/comments), and view presets.*
|
||||
|
||||
14. [ ] **Track: Context Preview & Slice Editor Fixes**
|
||||
14. [~] **Track: Context Preview & Slice Editor Fixes**
|
||||
*Link: [./tracks/context_preview_fixes_20260516/](./tracks/context_preview_fixes_20260516/)*
|
||||
*Goal: Fix Preview button generating empty content, and Inspect/Slices buttons failing to open their respective editor panels.*
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.resolve()))
|
||||
|
||||
import asyncio
|
||||
from simulation.sim_base import run_sim
|
||||
from simulation.sim_tools import click_tab, click_button_by_label
|
||||
from src.gui_2 import App
|
||||
from src.models import FileItem
|
||||
|
||||
async def verify_preview(app: App):
|
||||
# Let the GUI render once
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
app.context_files = [FileItem(path='src/aggregate.py', auto_aggregate=True)]
|
||||
app.controller.context_files = app.context_files
|
||||
|
||||
# Try rendering _render_files_and_media
|
||||
app.show_windows["Context Preview"] = True
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
click_tab(app, "Context Composition")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
click_button_by_label(app, "Preview##ctx")
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
print("--- PREVIEW TEXT ---")
|
||||
print(repr(app.context_preview_text[:200]))
|
||||
print("--------------------")
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ['SLOP_TEST_HOOKS'] = '1'
|
||||
run_sim(verify_preview)
|
||||
@@ -0,0 +1,81 @@
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Fix sys.path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(current_dir)
|
||||
src_dir = os.path.join(project_root, "src")
|
||||
|
||||
for d in [project_root, src_dir]:
|
||||
if d not in sys.path:
|
||||
sys.path.insert(0, d)
|
||||
|
||||
# Ensure thirdparty is in sys.path
|
||||
thirdparty = os.path.join(project_root, "thirdparty")
|
||||
if thirdparty not in sys.path:
|
||||
sys.path.insert(0, thirdparty)
|
||||
|
||||
import asyncio
|
||||
from simulation.sim_base import BaseSimulation, run_sim
|
||||
from src.gui_2 import App
|
||||
from src.models import FileItem
|
||||
|
||||
class VerifyTrackFixesSim(BaseSimulation):
|
||||
async def run(self):
|
||||
app = self.app
|
||||
print("[Sim] Starting verification of track fixes...")
|
||||
|
||||
# 1. Setup a test file in context
|
||||
test_file = os.path.abspath("src/aggregate.py")
|
||||
f_item = FileItem(path=test_file)
|
||||
app.context_files = [f_item]
|
||||
app.controller.context_files = app.context_files
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 2. Verify AST Inspector
|
||||
print("[Sim] Testing AST Inspector...")
|
||||
app.ui_inspecting_ast_file = f_item
|
||||
app._show_ast_inspector = True
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Check if nodes were cached
|
||||
if app._cached_ast_nodes:
|
||||
print(f"[Sim] SUCCESS: AST nodes cached ({len(app._cached_ast_nodes)} nodes)")
|
||||
else:
|
||||
print("[Sim] FAILURE: No AST nodes cached")
|
||||
|
||||
# 3. Verify Slice Editor
|
||||
print("[Sim] Testing Slice Editor...")
|
||||
app.ui_editing_slices_file = f_item
|
||||
app.text_viewer_title = f"Slices: {test_file}"
|
||||
from src import mcp_client
|
||||
app.text_viewer_content = mcp_client.read_file(test_file)
|
||||
app.text_viewer_type = "python"
|
||||
app.show_text_viewer = True
|
||||
app.show_windows["Text Viewer"] = True
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Test Auto-Populate button
|
||||
print("[Sim] Testing Auto-Populate AST Slices...")
|
||||
app._populate_auto_slices(f_item)
|
||||
if f_item.custom_slices:
|
||||
print(f"[Sim] SUCCESS: Auto-slices populated ({len(f_item.custom_slices)} slices)")
|
||||
else:
|
||||
print("[Sim] FAILURE: No auto-slices populated")
|
||||
|
||||
# 4. Verify Context Preview
|
||||
print("[Sim] Testing Context Preview...")
|
||||
app.context_preview_text = app.controller._do_generate()[0]
|
||||
if "## Files" in app.context_preview_text and "aggregate.py" in app.context_preview_text:
|
||||
print("[Sim] SUCCESS: Context preview generated with content")
|
||||
else:
|
||||
print(f"[Sim] FAILURE: Context preview text length: {len(app.context_preview_text)}")
|
||||
|
||||
print("[Sim] Verification finished.")
|
||||
self.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ['SLOP_TEST_HOOKS'] = '1'
|
||||
run_sim(VerifyTrackFixesSim)
|
||||
@@ -205,12 +205,20 @@ def build_file_items(base_dir: Path, files: list[str | dict[str, Any]]) -> list[
|
||||
if path.suffix == ".py":
|
||||
if not parser: parser = ASTParser("python")
|
||||
content = parser.get_skeleton(content, path=str(path))
|
||||
elif path.suffix in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
|
||||
from src import mcp_client
|
||||
if path.suffix in ['.c', '.h']: content = mcp_client.ts_c_get_skeleton(str(path))
|
||||
else: content = mcp_client.ts_cpp_get_skeleton(str(path))
|
||||
else:
|
||||
content = summarize.summarise_file(path, content)
|
||||
elif view_mode == "outline":
|
||||
if path.suffix == ".py":
|
||||
if not parser: parser = ASTParser("python")
|
||||
content = parser.get_code_outline(content, path=str(path))
|
||||
elif path.suffix in ['.c', '.h', '.cpp', '.hpp', '.cxx', '.cc']:
|
||||
from src import mcp_client
|
||||
if path.suffix in ['.c', '.h']: content = mcp_client.ts_c_get_code_outline(str(path))
|
||||
else: content = mcp_client.ts_cpp_get_code_outline(str(path))
|
||||
else:
|
||||
content = summarize.summarise_file(path, content)
|
||||
elif view_mode == "none":
|
||||
|
||||
@@ -955,6 +955,7 @@ class AppController:
|
||||
self.ui_new_track_name: str = ""
|
||||
self.ui_new_track_desc: str = ""
|
||||
self.ui_new_track_type: str = "feature"
|
||||
self.ui_project_conductor_dir: str = ""
|
||||
self.ui_conductor_setup_summary: str = ""
|
||||
self.ui_last_script_text: str = ""
|
||||
self.ui_last_script_output: str = ""
|
||||
|
||||
+62
-12
@@ -18,6 +18,13 @@ import time
|
||||
import tomli_w
|
||||
import typing
|
||||
from contextlib import ExitStack, nullcontext
|
||||
|
||||
# Ensure thirdparty is in sys.path for defer
|
||||
_project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_thirdparty = os.path.join(_project_root, "thirdparty")
|
||||
if _thirdparty not in sys.path:
|
||||
sys.path.insert(0, _thirdparty)
|
||||
|
||||
from defer import defer
|
||||
from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced
|
||||
from pathlib import Path
|
||||
@@ -923,8 +930,21 @@ class App:
|
||||
"""
|
||||
from src import mcp_client
|
||||
import re
|
||||
mcp_client.configure([{"path": f_item.path}])
|
||||
outline = mcp_client.py_get_code_outline(f_item.path)
|
||||
from pathlib import Path
|
||||
import os
|
||||
proj_dir = str(Path(self.controller.active_project_path).parent.resolve()) if getattr(self, 'controller', None) and self.controller.active_project_path else None
|
||||
abs_path = f_item.path if os.path.isabs(f_item.path) else os.path.join(proj_dir or '.', f_item.path)
|
||||
mcp_client.configure([{"path": abs_path}], [proj_dir] if proj_dir else None)
|
||||
|
||||
f_path_lower = f_item.path.lower()
|
||||
try:
|
||||
if f_path_lower.endswith('.py'): outline = mcp_client.py_get_code_outline(abs_path)
|
||||
elif f_path_lower.endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(abs_path)
|
||||
elif f_path_lower.endswith(('.cpp', '.hpp', '.cxx', '.cc')): outline = mcp_client.ts_cpp_get_code_outline(abs_path)
|
||||
else: return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if outline.startswith("ERROR") or outline.startswith("ACCESS DENIED"):
|
||||
return
|
||||
pattern = re.compile(r'^\s*\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)', re.MULTILINE)
|
||||
@@ -2841,8 +2861,13 @@ def render_context_batch_actions(app: App, total_lines: int, total_ast: int) ->
|
||||
if not app.context_files:
|
||||
app.context_preview_text = "# Context Composition Empty\n\nNo files have been added to the context composition yet."
|
||||
else:
|
||||
app.controller.context_files = app.context_files
|
||||
app.context_preview_text = app.controller._do_generate()[0]
|
||||
try:
|
||||
app.controller.context_files = app.context_files
|
||||
app.context_preview_text = app.controller._do_generate()[0]
|
||||
except Exception as e:
|
||||
import traceback
|
||||
err = traceback.format_exc()
|
||||
app.context_preview_text = f"# Error generating preview\n\n```python\n{err}\n```"
|
||||
app.show_windows["Context Preview"] = True
|
||||
imgui.same_line()
|
||||
imgui.text(f" | Total: {len(app.context_files)} files, {total_lines} lines, {total_ast} AST elements")
|
||||
@@ -2911,7 +2936,8 @@ def render_ast_inspector_modal(app: App) -> None:
|
||||
app._show_ast_inspector = False
|
||||
|
||||
#region: AST Inspector
|
||||
expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize)
|
||||
imgui.set_next_window_size(imgui.ImVec2(1200, 800), imgui.Cond_.first_use_ever)
|
||||
expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.none)
|
||||
if opened:
|
||||
if expanded:
|
||||
if app.ui_inspecting_ast_file is None:
|
||||
@@ -2923,6 +2949,11 @@ def render_ast_inspector_modal(app: App) -> None:
|
||||
if f_path != app._cached_ast_file_path:
|
||||
outline = ""
|
||||
try:
|
||||
from src import mcp_client
|
||||
from pathlib import Path
|
||||
proj_dir = str(Path(app.controller.active_project_path).parent.resolve()) if getattr(app, 'controller', None) and app.controller.active_project_path else None
|
||||
mcp_client.configure([{"path": f_path}], [proj_dir] if proj_dir else None)
|
||||
|
||||
if f_path.lower().endswith('.py'): outline = mcp_client.py_get_code_outline(f_path)
|
||||
elif f_path.lower().endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(f_path)
|
||||
else: outline = mcp_client.ts_cpp_get_code_outline(f_path)
|
||||
@@ -2959,12 +2990,14 @@ def render_ast_inspector_modal(app: App) -> None:
|
||||
imgui.text(f"Inspecting AST: {f_path}")
|
||||
imgui.separator()
|
||||
|
||||
avail = imgui.get_content_region_avail()
|
||||
table_height = max(100.0, avail.y - imgui.get_frame_height_with_spacing() - 10)
|
||||
#region: ast_dual_pane
|
||||
if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v):
|
||||
if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v, imgui.ImVec2(0, table_height)):
|
||||
imgui.table_next_column()
|
||||
|
||||
#region: LEFT COLUMN (Tree) ---
|
||||
imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True)
|
||||
imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 0), True)
|
||||
if True:
|
||||
if not app._cached_ast_nodes: imgui.text("No AST nodes found or error fetching outline.")
|
||||
else:
|
||||
@@ -2977,7 +3010,14 @@ def render_ast_inspector_modal(app: App) -> None:
|
||||
imgui.dummy(imgui.ImVec2(indent * 10, 0))
|
||||
imgui.same_line()
|
||||
imgui.text(f"[{kind}] {name}")
|
||||
imgui.same_line(imgui.get_window_width() - 200)
|
||||
|
||||
# Calculate space left and align radio buttons to the right
|
||||
btn_width = 150 # Estimated width of the 3 radio buttons
|
||||
avail_width = imgui.get_content_region_avail().x
|
||||
if avail_width > btn_width:
|
||||
imgui.same_line(imgui.get_window_width() - btn_width)
|
||||
else:
|
||||
imgui.same_line()
|
||||
|
||||
current_mode = f_item.ast_mask.get(full_path, 'hide')
|
||||
|
||||
@@ -2994,7 +3034,7 @@ def render_ast_inspector_modal(app: App) -> None:
|
||||
imgui.table_next_column()
|
||||
|
||||
#region: RIGHT COLUMN (Content) ---
|
||||
imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True)
|
||||
imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 0), True)
|
||||
if True:
|
||||
if not hasattr(app, '_cached_ast_file_lines') or not app._cached_ast_file_lines:
|
||||
imgui.text("No file content loaded.")
|
||||
@@ -3117,8 +3157,10 @@ def render_context_files_table(app: App) -> None:
|
||||
|
||||
f_path = f_item.path if hasattr(f_item, "path") else str(f_item)
|
||||
is_sel = f_path in app.ui_selected_context_files
|
||||
f_item.auto_aggregate = is_sel
|
||||
changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel)
|
||||
if changed_sel:
|
||||
f_item.auto_aggregate = is_sel
|
||||
if imgui.get_io().key_shift and app._last_selected_context_index != -1:
|
||||
start = min(app._last_selected_context_index, i)
|
||||
end = max(app._last_selected_context_index, i)
|
||||
@@ -4104,6 +4146,9 @@ def render_text_viewer_window(app: App) -> None:
|
||||
app._slice_sel_start = -1; app._slice_sel_end = -1
|
||||
imgui.same_line()
|
||||
if imgui.button("Clear Selection"): app._slice_sel_start = -1; app._slice_sel_end = -1
|
||||
imgui.same_line()
|
||||
if imgui.button("Auto-Populate AST Slices"): app._populate_auto_slices(app.ui_editing_slices_file)
|
||||
|
||||
to_remove = -1
|
||||
for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices):
|
||||
imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line()
|
||||
@@ -4127,14 +4172,19 @@ def render_text_viewer_window(app: App) -> None:
|
||||
lines = app.text_viewer_content.splitlines(); draw_list = imgui.get_window_draw_list()
|
||||
for i, line_text in enumerate(lines):
|
||||
line_num = i + 1; pos = imgui.get_cursor_screen_pos(); line_height = imgui.get_text_line_height()
|
||||
is_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in app.ui_editing_slices_file.custom_slices)
|
||||
if is_sliced: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(255, 165, 0, 0.2)))
|
||||
|
||||
is_auto_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in app.ui_editing_slices_file.custom_slices if slc.get('tag') == 'auto-ast')
|
||||
is_manual_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in app.ui_editing_slices_file.custom_slices if slc.get('tag') != 'auto-ast')
|
||||
|
||||
if is_manual_sliced: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(255, 165, 0, 0.2)))
|
||||
elif is_auto_sliced: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.15)))
|
||||
|
||||
if app._slice_sel_start != -1 and app._slice_sel_end != -1:
|
||||
s, e = min(app._slice_sel_start, app._slice_sel_end), max(app._slice_sel_start, app._slice_sel_end)
|
||||
if s <= line_num <= e: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(100, 100, 255, 0.3)))
|
||||
imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False)
|
||||
if imgui.is_item_clicked(): app._slice_sel_start = line_num; app._slice_sel_end = line_num
|
||||
if imgui.is_item_hovered() and imgui.is_mouse_down(0): app._slice_sel_end = line_num
|
||||
if imgui.is_item_hovered(imgui.HoveredFlags_.allow_when_blocked_by_active_item) and imgui.is_mouse_down(0): app._slice_sel_end = line_num
|
||||
elif tv_type in renderer._lang_map:
|
||||
if app._text_viewer_editor is None:
|
||||
app._text_viewer_editor = ced.TextEditor(); app._text_viewer_editor.set_read_only_enabled(True); app._text_viewer_editor.set_show_line_numbers_enabled(True)
|
||||
|
||||
@@ -26,6 +26,8 @@ def test_ast_inspector_line_range_parsing():
|
||||
mock_imgui.begin_child.return_value = True
|
||||
# radio_button returns (changed, active)
|
||||
mock_imgui.radio_button.return_value = (False, False)
|
||||
mock_imgui.get_content_region_avail.return_value.y = 800.0
|
||||
mock_imgui.get_frame_height_with_spacing.return_value = 24.0
|
||||
|
||||
# Setup imscope mocks
|
||||
mock_imscope.window.return_value.__enter__.return_value = (True, True)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import pytest
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
def test_context_preview_and_ast_inspector(live_gui):
|
||||
process, script_path = live_gui
|
||||
|
||||
# Give it a second to stabilize
|
||||
time.sleep(1)
|
||||
|
||||
# Set context file
|
||||
resp = requests.post("http://127.0.0.1:8999/api/gui", json={
|
||||
"action": "set_value",
|
||||
"item": "files",
|
||||
"value": ["src/aggregate.py"]
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
time.sleep(1)
|
||||
|
||||
# Trigger Context Preview
|
||||
resp = requests.post("http://127.0.0.1:8999/api/gui", json={
|
||||
"action": "click",
|
||||
"item": "btn_preview_ctx" # wait, there is no btn_preview_ctx registered in _clickable_actions
|
||||
})
|
||||
|
||||
# Wait, the best way to verify if _do_generate works without crashing is
|
||||
# checking /api/v1/context
|
||||
resp = requests.get("http://127.0.0.1:8999/api/v1/context")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "## Files" in data["markdown"]
|
||||
assert "aggregate.py" in data["markdown"]
|
||||
print("SUCCESS: Context Generation works.")
|
||||
Reference in New Issue
Block a user