diff --git a/docs/edit_workflow.md b/docs/edit_workflow.md new file mode 100644 index 0000000..9fd657d --- /dev/null +++ b/docs/edit_workflow.md @@ -0,0 +1,130 @@ +# Manual Slop Edit Tool Workflow + +## The Problem +The `manual-slop_edit_file` tool requires **exact string matches** (character-for-character). Whitespace differences cause failures. The Python file uses **1-space indentation**. + +## The Rules + +### 1. ALWAYS Use Small, Incremental Edits +**WRONG:** Replace large blocks (50+ lines) +**RIGHT:** Replace 3-10 lines at a time, verify, repeat + +### 2. Verify Before Editing +Before ANY edit to a function you haven't touched recently: +``` +1. Run: git checkout -- src/gui_2.py +2. Run: py_check_syntax on src/gui_2.py +3. Get current state with get_file_slice +``` + +### 3. Reading Before Editing (CRITICAL) +- Use `get_file_slice` to get the EXACT text including all whitespace +- Copy text directly from the tool output - do NOT reformat +- If using get_definition, verify the text matches before editing + +### 4. The Edit Tool Parameters (snake_case) +```python +{ + "path": "src/gui_2.py", # Required: file path + "old_string": "exact text", # Required: must match EXACTLY + "new_string": "replacement", # Required: replacement text + "replace_all": false # Optional: replace all occurrences +} +``` + +### 5. 1-Space Indentation in Python +- Class methods: ` def` (0 spaces, then 1) +- Method body: ` ` (2 spaces total) +- Nested blocks: ` ` (3 spaces total) +- NO 4-space indentation anywhere in this file + +## Step-by-Step Workflow for gui_2.py + +### Before ANY edit: +```powershell +git checkout -- src/gui_2.py +``` + +### Check current state: +```powershell +py_check_syntax path=src/gui_2.py +get_file_slice path=src/gui_2.py start_line=X end_line=Y +``` + +### For each edit: +1. Make the smallest possible change (3-10 lines) +2. Run `py_check_syntax` to verify +3. If syntax error, immediately `git checkout -- src/gui_2.py` +4. Only proceed if syntax is OK + +### If edit fails with "old_string not found": +- The text you're trying to replace doesn't EXACTLY match +- Use `get_file_slice` to get the exact text +- Copy it character-for-character including whitespace +- Try again with exact match + +### If syntax error after edit: +```powershell +git checkout -- src/gui_2.py +``` +Then try again with smaller edit. + +## Alternative: Update Definition Approach + +For large function rewrites, use `py_update_definition`: +``` +name: function_name +path: src/gui_2.py +new_content: complete new function source +``` + +This replaces the entire function at once using AST detection. + +## Context Composition Requirements + +### Current Broken State +Files & Media works. Context Composition needs: + +1. Add state tracking at start of function: +```python +if not hasattr(self, 'ctx_files_open'): + self.ctx_files_open = True +if not hasattr(self, 'ctx_shots_open'): + self.ctx_shots_open = True +``` + +2. Files section with collapsing header and child window: +```python +if imgui.collapsing_header("Files", self.ctx_files_open): + imgui.begin_child("ctx_files_child", imgui.ImVec2(-1, 200), True) + # table code here + imgui.end_child() +``` + +3. Screenshots section with collapsing header and child window: +```python +if imgui.collapsing_header("Screenshots", self.ctx_shots_open): + imgui.begin_child("ctx_shots_child", imgui.ImVec2(-1, 100), True) + # screenshot list here + imgui.end_child() +``` + +4. Fixed presets bar with push_item_width(150) on the combo + +5. Remove the batch action bar entirely (Full/Agg/Sig/Def/None/Sel All/Del buttons) + +## Key Files +- `src/gui_2.py` - Main GUI (1-space indentation, CRLF) +- `src/models.py` - Data models including FileItem +- Context Composition function: line ~2748 + +## Test Command +```powershell +uv run sloppy.py +``` + +## If Everything Goes Wrong +```powershell +git checkout -- src/gui_2.py +git checkout -- src/models.py +``` \ No newline at end of file diff --git a/src/gui_2.py b/src/gui_2.py index 02bd3d2..6ceb12f 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -2747,185 +2747,175 @@ class App: imgui.text_disabled("Message & Response panels are detached.") def _render_context_composition_panel(self) -> None: - imgui.text("Context Composition") - imgui.separator() - - def _batch_helper(name: str, attr: str): + if imgui.collapsing_header("Context Composition"): + #region: Batch Action Bar + imgui.text("Batch:") imgui.same_line() - if imgui.button(name + "##batch"): + if imgui.button("Full##batch"): for f in self.files: f_path = f.path if hasattr(f, "path") else str(f) if f_path in self.ui_selected_context_files: f.force_full = True f.auto_aggregate = False - if hasattr(f, attr): + if hasattr(f, "ast_signatures"): f.ast_signatures = False f.ast_definitions = False - - # Batch Action Bar - imgui.text("Batch:") - imgui.same_line() - if imgui.button("Full##batch"): - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in self.ui_selected_context_files: - f.force_full = True - f.auto_aggregate = False - if hasattr(f, "ast_signatures"): - f.ast_signatures = False - f.ast_definitions = False - imgui.same_line() - if imgui.button("Agg##batch"): - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in self.ui_selected_context_files: - f.auto_aggregate = True - f.force_full = False - if hasattr(f, "ast_signatures"): - f.ast_signatures = False - f.ast_definitions = False - imgui.same_line() - if imgui.button("Sig##batch"): - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in self.ui_selected_context_files: - if hasattr(f, "ast_signatures"): - f.ast_signatures = True + imgui.same_line() + if imgui.button("Agg##batch"): + for f in self.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path in self.ui_selected_context_files: + f.auto_aggregate = True f.force_full = False + if hasattr(f, "ast_signatures"): + f.ast_signatures = False + f.ast_definitions = False + imgui.same_line() + if imgui.button("Sig##batch"): + for f in self.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path in self.ui_selected_context_files: + if hasattr(f, "ast_signatures"): + f.ast_signatures = True + f.force_full = False + f.auto_aggregate = False + f.ast_definitions = False + imgui.same_line() + if imgui.button("Def##batch"): + for f in self.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path in self.ui_selected_context_files: + if hasattr(f, "ast_definitions"): + f.ast_definitions = True + f.force_full = False + f.auto_aggregate = False + f.ast_signatures = False + imgui.same_line() + if imgui.button("None##batch"): + for f in self.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path in self.ui_selected_context_files: f.auto_aggregate = False - f.ast_definitions = False - imgui.same_line() - if imgui.button("Def##batch"): - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in self.ui_selected_context_files: - if hasattr(f, "ast_definitions"): - f.ast_definitions = True f.force_full = False - f.auto_aggregate = False - f.ast_signatures = False - imgui.same_line() - if imgui.button("None##batch"): - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in self.ui_selected_context_files: - f.auto_aggregate = False - f.force_full = False - if hasattr(f, "ast_signatures"): - f.ast_signatures = False - f.ast_definitions = False - imgui.same_line() - if imgui.button("Sel All##selall"): - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - self.ui_selected_context_files.add(f_path) - imgui.same_line() - if imgui.button("Unsel All##unselall"): - self.ui_selected_context_files.clear() - imgui.same_line() - if imgui.button("Del##batch"): - new_files = [] - for f in self.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path not in self.ui_selected_context_files: - new_files.append(f) - self.files = new_files - self.ui_selected_context_files.clear() - - imgui.dummy(imgui.ImVec2(0, 4)) + if hasattr(f, "ast_signatures"): + f.ast_signatures = False + f.ast_definitions = False + imgui.same_line() + if imgui.button("Sel All##selall"): + for f in self.files: + f_path = f.path if hasattr(f, "path") else str(f) + self.ui_selected_context_files.add(f_path) + imgui.same_line() + if imgui.button("Unsel All##unselall"): + self.ui_selected_context_files.clear() + imgui.same_line() + if imgui.button("Del##batch"): + new_files = [] + for f in self.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path not in self.ui_selected_context_files: + new_files.append(f) + self.files = new_files + self.ui_selected_context_files.clear() + #endregion: Batch Action Bar + + imgui.dummy(imgui.ImVec2(0, 4)) - if imgui.begin_table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): - imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) - imgui.table_headers_row() - for i, f_item in enumerate(self.files): - imgui.table_next_row() - imgui.table_set_column_index(0) - - # Checkbox for selection - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - is_sel = f_path in self.ui_selected_context_files - changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) - if changed_sel: - if imgui.get_io().key_shift and self._last_selected_context_index != -1: - start = min(self._last_selected_context_index, i) - end = max(self._last_selected_context_index, i) - for idx in range(start, end + 1): - item = self.files[idx] - item_path = item.path if hasattr(item, "path") else str(item) - if is_sel: - self.ui_selected_context_files.add(item_path) - else: - self.ui_selected_context_files.discard(item_path) - else: - if is_sel: - self.ui_selected_context_files.add(f_path) - else: - self.ui_selected_context_files.discard(f_path) - self._last_selected_context_index = i - imgui.same_line() - imgui.text(f_path) - - if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): - imgui.same_line() - if imgui.button(f"[Inspect]##{i}"): - self.ui_inspecting_ast_file = f_item - imgui.open_popup('AST Inspector') - - imgui.same_line() - if imgui.button(f"[Slices]##{i}"): - self.ui_editing_slices_file = f_item + if imgui.begin_table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): + imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) + imgui.table_headers_row() + for i, f_item in enumerate(self.files): + imgui.table_next_row() + imgui.table_set_column_index(0) + + # Checkbox for selection f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - self.text_viewer_title = f"Slices: {f_path}" - self.text_viewer_content = f_item.content or "" - self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' - self.show_text_viewer = True - - imgui.table_set_column_index(1) - if hasattr(f_item, "auto_aggregate"): - changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##cc{i}", f_item.auto_aggregate) + is_sel = f_path in self.ui_selected_context_files + changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) + if changed_sel: + if imgui.get_io().key_shift and self._last_selected_context_index != -1: + start = min(self._last_selected_context_index, i) + end = max(self._last_selected_context_index, i) + for idx in range(start, end + 1): + item = self.files[idx] + item_path = item.path if hasattr(item, "path") else str(item) + if is_sel: + self.ui_selected_context_files.add(item_path) + else: + self.ui_selected_context_files.discard(item_path) + else: + if is_sel: + self.ui_selected_context_files.add(f_path) + else: + self.ui_selected_context_files.discard(f_path) + self._last_selected_context_index = i imgui.same_line() - changed_full, f_item.force_full = imgui.checkbox(f"Full##cc{i}", f_item.force_full) - if hasattr(f_item, "ast_signatures"): + imgui.text(f_path) + + if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): imgui.same_line() - _, f_item.ast_signatures = imgui.checkbox(f"Sig##cc{i}", f_item.ast_signatures) + if imgui.button(f"[Inspect]##{i}"): + self.ui_inspecting_ast_file = f_item + imgui.open_popup('AST Inspector') + + imgui.same_line() + if imgui.button(f"[Slices]##{i}"): + self.ui_editing_slices_file = f_item + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + self.text_viewer_title = f"Slices: {f_path}" + self.text_viewer_content = f_item.content or "" + self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' + self.show_text_viewer = True + + imgui.table_set_column_index(1) + if hasattr(f_item, "auto_aggregate"): + changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##cc{i}", f_item.auto_aggregate) imgui.same_line() - _, f_item.ast_definitions = imgui.checkbox(f"Def##cc{i}", f_item.ast_definitions) - imgui.end_table() + changed_full, f_item.force_full = imgui.checkbox(f"Full##cc{i}", f_item.force_full) + if hasattr(f_item, "ast_signatures"): + imgui.same_line() + _, f_item.ast_signatures = imgui.checkbox(f"Sig##cc{i}", f_item.ast_signatures) + imgui.same_line() + _, f_item.ast_definitions = imgui.checkbox(f"Def##cc{i}", f_item.ast_definitions) + imgui.end_table() + # Context Composition collasping header + imgui.separator() - imgui.text("Screenshots") - for i, s in enumerate(self.screenshots): - imgui.text(s) - imgui.separator() - imgui.text("Presets") - presets = self.controller.project.get('context_presets', {}) - preset_names = [""] + sorted(presets.keys()) - active = getattr(self, "ui_active_context_preset", "") - if active not in preset_names: - active = "" - try: - idx = preset_names.index(active) - except ValueError: - idx = 0 - ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names) - if ch: - self.ui_active_context_preset = preset_names[new_idx] - if preset_names[new_idx]: - self.load_context_preset(preset_names[new_idx]) - imgui.same_line() - changed, new_name = imgui.input_text("##new_preset", getattr(self, "ui_new_context_preset_name", "")) - if changed: - self.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save##ctx"): - if getattr(self, "ui_new_context_preset_name", "").strip(): - self.save_context_preset(self.ui_new_context_preset_name.strip()) - self.ui_new_context_preset_name = "" - imgui.same_line() - if imgui.button("Delete##ctx"): - if getattr(self, "ui_active_context_preset", ""): - self.delete_context_preset(self.ui_active_context_preset) - self.ui_active_context_preset = "" + + if imgui.collapsing_header("Screenshots"): + for i, s in enumerate(self.screenshots): + imgui.text(s) + imgui.separator() + imgui.text("Presets") + presets = self.controller.project.get('context_presets', {}) + preset_names = [""] + sorted(presets.keys()) + active = getattr(self, "ui_active_context_preset", "") + if active not in preset_names: + active = "" + try: + idx = preset_names.index(active) + except ValueError: + idx = 0 + ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names) + if ch: + self.ui_active_context_preset = preset_names[new_idx] + if preset_names[new_idx]: + self.load_context_preset(preset_names[new_idx]) + imgui.same_line() + changed, new_name = imgui.input_text("##new_preset", getattr(self, "ui_new_context_preset_name", "")) + if changed: + self.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save##ctx"): + if getattr(self, "ui_new_context_preset_name", "").strip(): + self.save_context_preset(self.ui_new_context_preset_name.strip()) + self.ui_new_context_preset_name = "" + imgui.same_line() + if imgui.button("Delete##ctx"): + if getattr(self, "ui_active_context_preset", ""): + self.delete_context_preset(self.ui_active_context_preset) + self.ui_active_context_preset = "" def _render_snapshot_tab(self) -> None: if imgui.begin_tab_bar("snapshot_tabs"):