feat(ui): Implement manual skeleton context injection

This commit is contained in:
2026-03-07 11:54:11 -05:00
parent 442d5d23b6
commit fbe02ebfd4
5 changed files with 145 additions and 5 deletions

View File

@@ -56,7 +56,7 @@ This file tracks all major tracks for the project. Each track has its own detail
11. [x] **Track: Track Progress Visualization**
*Link: [./tracks/track_progress_viz_20260306/](./tracks/track_progress_viz_20260306/)*
12. [ ] **Track: Manual Skeleton Context Injection**
12. [~] **Track: Manual Skeleton Context Injection**
*Link: [./tracks/manual_skeleton_injection_20260306/](./tracks/manual_skeleton_injection_20260306/)*
13. [ ] **Track: On-Demand Definition Lookup**

View File

@@ -5,10 +5,10 @@
## Phase 1: UI Foundation
Focus: Add file injection button and state
- [ ] Task 1.1: Initialize MMA Environment
- [x] Task 1.1: Initialize MMA Environment
- Run `activate_skill mma-orchestrator` before starting
- [ ] Task 1.2: Add injection state variables
- [~] Task 1.2: Add injection state variables
- WHERE: `src/gui_2.py` `App.__init__`
- WHAT: State for injection UI
- HOW:

View File

@@ -270,6 +270,11 @@ class AppController:
self.prior_session_entries: List[Dict[str, Any]] = []
self.test_hooks_enabled: bool = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1")
self.ui_manual_approve: bool = False
# Injection state
self._inject_file_path: str = ""
self._inject_mode: str = "skeleton"
self._inject_preview: str = ""
self._show_inject_modal: bool = False
self._settable_fields: Dict[str, str] = {
'ai_input': 'ui_ai_input',
'project_git_dir': 'ui_project_git_dir',
@@ -293,7 +298,10 @@ class AppController:
'mma_active_tier': 'active_tier',
'ui_new_track_name': 'ui_new_track_name',
'ui_new_track_desc': 'ui_new_track_desc',
'manual_approve': 'ui_manual_approve'
'manual_approve': 'ui_manual_approve',
'inject_file_path': '_inject_file_path',
'inject_mode': '_inject_mode',
'show_inject_modal': '_show_inject_modal'
}
self._gettable_fields = dict(self._settable_fields)
self._gettable_fields.update({
@@ -311,10 +319,40 @@ class AppController:
'prior_session_indicator': 'prior_session_indicator',
'_show_patch_modal': '_show_patch_modal',
'_pending_patch_text': '_pending_patch_text',
'_pending_patch_files': '_pending_patch_files'
'_pending_patch_files': '_pending_patch_files',
'_inject_file_path': '_inject_file_path',
'_inject_mode': '_inject_mode',
'_inject_preview': '_inject_preview',
'_show_inject_modal': '_show_inject_modal'
})
self._init_actions()
def _update_inject_preview(self) -> None:
"""Updates the preview content based on the selected file and injection mode."""
if not self._inject_file_path:
self._inject_preview = ""
return
target_path = self._inject_file_path
if not os.path.isabs(target_path):
target_path = os.path.join(self.ui_files_base_dir, target_path)
if not os.path.exists(target_path):
self._inject_preview = ""
return
try:
with open(target_path, "r", encoding="utf-8") as f:
content = f.read()
if self._inject_mode == "skeleton" and target_path.endswith(".py"):
parser = ASTParser("python")
preview = parser.get_skeleton(content)
else:
preview = content
lines = preview.splitlines()
if len(lines) > 500:
preview = "\n".join(lines[:500]) + "\n... (truncated)"
self._inject_preview = preview
except Exception as e:
self._inject_preview = f"Error reading file: {e}"
@property
def thinking_indicator(self) -> bool:
return self.ai_status in ("sending...", "streaming...")

View File

@@ -744,6 +744,42 @@ class App:
else:
imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only)
imgui.end()
# Inject File Modal
if getattr(self, "show_inject_modal", False):
imgui.open_popup("Inject File")
self.show_inject_modal = False
if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]:
files = self.project.get('files', {}).get('paths', [])
imgui.text("Select File to Inject:")
imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True)
for f_path in files:
is_selected = (self._inject_file_path == f_path)
if imgui.selectable(f_path, is_selected)[0]:
self._inject_file_path = f_path
self.controller._update_inject_preview()
imgui.end_child()
imgui.separator()
if imgui.radio_button("Skeleton", self._inject_mode == "skeleton"):
self._inject_mode = "skeleton"
self.controller._update_inject_preview()
imgui.same_line()
if imgui.radio_button("Full", self._inject_mode == "full"):
self._inject_mode = "full"
self.controller._update_inject_preview()
imgui.separator()
imgui.text("Preview:")
imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True)
imgui.text_unformatted(self._inject_preview)
imgui.end_child()
imgui.separator()
if imgui.button("Inject", imgui.ImVec2(120, 0)):
formatted = f"## File: {self._inject_file_path}\n```python\n{self._inject_preview}\n```\n"
self.ui_ai_input += formatted
imgui.close_current_popup()
imgui.same_line()
if imgui.button("Cancel", imgui.ImVec2(120, 0)):
imgui.close_current_popup()
imgui.end_popup()
except Exception as e:
print(f"ERROR in _gui_func: {e}")
import traceback
@@ -1543,6 +1579,9 @@ class App:
with self._send_thread_lock:
if self.send_thread and self.send_thread.is_alive():
send_busy = True
if imgui.button("Inject File"):
self.show_inject_modal = True
imgui.same_line()
if (imgui.button("Gen + Send") or ctrl_enter) and not send_busy:
self._handle_generate_send()
imgui.same_line()

View File

@@ -0,0 +1,63 @@
import os
import pytest
from src.app_controller import AppController
from pathlib import Path
def test_skeleton_injection_state_variables():
ctrl = AppController()
assert hasattr(ctrl, "_inject_file_path")
assert hasattr(ctrl, "_inject_mode")
assert hasattr(ctrl, "_inject_preview")
assert hasattr(ctrl, "_show_inject_modal")
assert ctrl._inject_mode == "skeleton"
assert ctrl._inject_preview == ""
assert ctrl._show_inject_modal is False
def test_update_inject_preview_skeleton(tmp_path):
ctrl = AppController()
mock_file = tmp_path / "mock.py"
# Use 1-space indent in the mock content too for consistency
content = '"""Module docstring"""\ndef foo():\n """Foo docstring"""\n print("hello")\n\nclass Bar:\n """Bar docstring"""\n def baz(self):\n pass\n'
mock_file.write_text(content)
ctrl._inject_file_path = str(mock_file)
ctrl._inject_mode = "skeleton"
ctrl._update_inject_preview()
# Skeleton should contain signatures and docstrings but not bodies
assert "def foo():" in ctrl._inject_preview
assert "class Bar:" in ctrl._inject_preview
assert 'print("hello")' not in ctrl._inject_preview
assert "pass" not in ctrl._inject_preview
assert '"""Foo docstring"""' in ctrl._inject_preview
def test_update_inject_preview_full(tmp_path):
ctrl = AppController()
mock_file = tmp_path / "mock.py"
content = "line 1\n" * 10
mock_file.write_text(content)
ctrl._inject_file_path = str(mock_file)
ctrl._inject_mode = "full"
ctrl._update_inject_preview()
assert ctrl._inject_preview.strip() == content.strip()
def test_update_inject_preview_truncation(tmp_path):
ctrl = AppController()
mock_file = tmp_path / "large.py"
content = "\n".join([f"line {i}" for i in range(1000)])
mock_file.write_text(content)
ctrl._inject_file_path = str(mock_file)
ctrl._inject_mode = "full"
ctrl._update_inject_preview()
lines = ctrl._inject_preview.splitlines()
# It should be 500 lines + 1 for truncated message
assert len(lines) == 501
assert "... (truncated)" in lines[-1]
# The 500th line is "line 499" (starting from 0)
assert "line 499" in lines[499]
# The 501st line should be "line 500", but it should be replaced by truncation msg
assert "line 500" not in ctrl._inject_preview