diff --git a/conductor/tracks/saved_presets_20260308/plan.md b/conductor/tracks/saved_presets_20260308/plan.md index 36b3101..8b08766 100644 --- a/conductor/tracks/saved_presets_20260308/plan.md +++ b/conductor/tracks/saved_presets_20260308/plan.md @@ -41,6 +41,9 @@ ## Phase 4: Final Integration & Polish - [x] Task: Ensure robust error handling for missing or malformed `.toml` files. +- [x] Task: Bugfix: Correct `PresetManager` initialization to use project parent directory. +- [x] Task: Hardening: Wrap modal rendering in `try...finally` to prevent ImGui state corruption. +- [x] Task: Hardening: Ensure `PresetManager._save_file` validates that parent is a directory. - [x] Task: Final UI polish (spacing, icons, tooltips). - [x] Task: Run full suite of relevant tests. - [x] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md) diff --git a/config.toml b/config.toml index e5da747..a31d916 100644 --- a/config.toml +++ b/config.toml @@ -2,9 +2,10 @@ provider = "minimax" model = "MiniMax-M2.5" temperature = 0.0 -max_tokens = 24000 +max_tokens = 4096 history_trunc_limit = 900000 -system_prompt = "" +active_preset = "Default" +system_prompt = "Not sure yet." [projects] paths = [ @@ -40,7 +41,7 @@ Response = false "Tool Calls" = false Theme = true "Log Management" = true -Diagnostics = true +Diagnostics = false [theme] palette = "Nord Dark" diff --git a/manualslop_layout.ini b/manualslop_layout.ini index a603074..b9ec079 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -114,14 +114,14 @@ Collapsed=0 DockId=0x00000012,0 [Window][Files & Media] -Pos=0,1849 -Size=762,288 +Pos=0,2013 +Size=762,124 Collapsed=0 DockId=0x00000002,0 [Window][AI Settings] Pos=0,975 -Size=762,872 +Size=762,1036 Collapsed=0 DockId=0x00000001,0 @@ -321,6 +321,11 @@ Pos=755,679 Size=420,966 Collapsed=0 +[Window][Preset Manager] +Pos=786,858 +Size=780,650 +Collapsed=0 + [Table][0xFB6E3870,4] RefScale=13 Column 0 Width=80 @@ -415,8 +420,8 @@ DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,30 Size=3840,2107 Spli DockNode ID=0x00000007 Parent=0x0000000B SizeRef=762,858 Split=Y Selected=0x8CA2375C DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,943 Selected=0xF4139CA2 DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,1168 Split=Y Selected=0x7BD57D6A - DockNode ID=0x00000001 Parent=0x00000006 SizeRef=824,872 CentralNode=1 Selected=0x7BD57D6A - DockNode ID=0x00000002 Parent=0x00000006 SizeRef=824,288 Selected=0x1DCB2623 + DockNode ID=0x00000001 Parent=0x00000006 SizeRef=824,1036 CentralNode=1 Selected=0x7BD57D6A + DockNode ID=0x00000002 Parent=0x00000006 SizeRef=824,124 Selected=0x1DCB2623 DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1908,858 Split=X Selected=0x418C7449 DockNode ID=0x00000012 Parent=0x0000000E SizeRef=902,402 Selected=0x418C7449 DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1004,402 Selected=0x6F2B5B04 diff --git a/presets.toml b/presets.toml new file mode 100644 index 0000000..ba90548 --- /dev/null +++ b/presets.toml @@ -0,0 +1,5 @@ +[presets.Default] +system_prompt = "Not sure yet." +temperature = 0.0 +top_p = 1.0 +max_output_tokens = 4096 diff --git a/src/app_controller.py b/src/app_controller.py index 243f3ef..1d16c8a 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -822,7 +822,7 @@ class AppController: self.ui_auto_add_history = disc_sec.get("auto_add", False) self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "") - self.preset_manager = presets.PresetManager(Path(self.active_project_path) if self.active_project_path else None) + self.preset_manager = presets.PresetManager(Path(self.active_project_path).parent if self.active_project_path else None) self.presets = self.preset_manager.load_all() self.ui_global_preset_name = ai_cfg.get("active_preset") self.ui_project_preset_name = proj_meta.get("active_preset") @@ -1793,6 +1793,8 @@ class AppController: self.max_tokens = preset.max_output_tokens def _cb_save_preset(self, name, content, temp, top_p, max_tok, scope): + if not name or not name.strip(): + raise ValueError("Preset name cannot be empty or whitespace.") preset = models.Preset( name=name, system_prompt=content, diff --git a/src/gui_2.py b/src/gui_2.py index 7db9a49..ee76e0a 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -409,13 +409,13 @@ class App: if exp: if imgui.collapsing_header("Provider & Model"): self._render_provider_panel() + if imgui.collapsing_header("System Prompts"): + self._render_system_prompts_panel() if imgui.collapsing_header("Token Budget"): self._render_token_budget_panel() self._render_cache_panel() self._render_tool_analytics_panel() self._render_session_insights_panel() - if imgui.collapsing_header("System Prompts"): - self._render_system_prompts_panel() imgui.end() if self.show_windows.get("MMA Dashboard", False): @@ -864,81 +864,89 @@ class App: def _render_preset_manager_modal(self) -> None: if not self.show_preset_manager_modal: return imgui.open_popup("Preset Manager") - if imgui.begin_popup_modal("Preset Manager", True, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.begin_child("preset_list_area", imgui.ImVec2(250, 600), True) - preset_names = sorted(self.controller.presets.keys()) - if imgui.button("New Preset", imgui.ImVec2(-1, 0)): - self._editing_preset_name = "" - self._editing_preset_content = "" - self._editing_preset_temperature = 0.0 - self._editing_preset_top_p = 1.0 - self._editing_preset_max_output_tokens = 4096 - self._editing_preset_scope = "project" - self._editing_preset_is_new = True - imgui.separator() - for name in preset_names: - p = self.controller.presets[name] - is_sel = (name == self._editing_preset_name) - if imgui.selectable(name, is_sel)[0]: - self._editing_preset_name = name - self._editing_preset_content = p.system_prompt - self._editing_preset_temperature = p.temperature if p.temperature is not None else 0.0 - self._editing_preset_top_p = p.top_p if p.top_p is not None else 1.0 - self._editing_preset_max_output_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096 - self._editing_preset_is_new = False - imgui.end_child() - imgui.same_line() - imgui.begin_child("preset_edit_area", imgui.ImVec2(500, 600), False) - p_name = self._editing_preset_name or "(New Preset)" - imgui.text_colored(C_IN, f"Editing Preset: {p_name}") - imgui.separator() - imgui.text("Name:") - _, self._editing_preset_name = imgui.input_text("##edit_name", self._editing_preset_name) - imgui.text("Scope:") - if imgui.radio_button("Global", self._editing_preset_scope == "global"): - self._editing_preset_scope = "global" - imgui.same_line() - if imgui.radio_button("Project", self._editing_preset_scope == "project"): - self._editing_preset_scope = "project" - imgui.text("Content:") - _, self._editing_preset_content = imgui.input_text_multiline("##edit_content", self._editing_preset_content, imgui.ImVec2(-1, 280)) - - imgui.text("Temperature:") - _, self._editing_preset_temperature = imgui.input_float("##edit_temp", self._editing_preset_temperature, 0.1, 1.0, "%.2f") - imgui.text("Top P:") - _, self._editing_preset_top_p = imgui.input_float("##edit_top_p", self._editing_preset_top_p, 0.1, 1.0, "%.2f") - imgui.text("Max Output Tokens:") - _, self._editing_preset_max_output_tokens = imgui.input_int("##edit_max_tokens", self._editing_preset_max_output_tokens) - - if imgui.button("Save", imgui.ImVec2(120, 0)): - if self._editing_preset_name.strip(): - self.controller._cb_save_preset( - self._editing_preset_name.strip(), - self._editing_preset_content, - self._editing_preset_temperature, - self._editing_preset_top_p, - self._editing_preset_max_output_tokens, - self._editing_preset_scope - ) - self.ai_status = f"Preset '{self._editing_preset_name.strip()}' saved to {self._editing_preset_scope}" - imgui.set_item_tooltip("Save the current preset settings") - imgui.same_line() - if imgui.button("Delete", imgui.ImVec2(120, 0)): - if self._editing_preset_name.strip(): - try: - self.controller._cb_delete_preset(self._editing_preset_name.strip(), self._editing_preset_scope) - self.ai_status = f"Preset '{self._editing_preset_name}' deleted from {self._editing_preset_scope}" + opened, self.show_preset_manager_modal = imgui.begin_popup_modal("Preset Manager", self.show_preset_manager_modal) + if opened: + try: + avail = imgui.get_content_region_avail() + imgui.begin_child("preset_list_area", imgui.ImVec2(250, avail.y), True) + try: + preset_names = sorted(self.controller.presets.keys()) + if imgui.button("New Preset", imgui.ImVec2(-1, 0)): self._editing_preset_name = "" self._editing_preset_content = "" - except Exception as e: - self.ai_status = f"Error deleting: {e}" - imgui.set_item_tooltip("Delete the selected preset") - imgui.same_line() - if imgui.button("Close", imgui.ImVec2(120, 0)): - self.show_preset_manager_modal = False - imgui.close_current_popup() - imgui.end_child() - imgui.end_popup() + self._editing_preset_temperature = 0.0 + self._editing_preset_top_p = 1.0 + self._editing_preset_max_output_tokens = 4096 + self._editing_preset_scope = "project" + self._editing_preset_is_new = True + imgui.separator() + for name in preset_names: + p = self.controller.presets[name] + is_sel = (name == self._editing_preset_name) + if imgui.selectable(name, is_sel)[0]: + self._editing_preset_name = name + self._editing_preset_content = p.system_prompt + self._editing_preset_temperature = p.temperature if p.temperature is not None else 0.0 + self._editing_preset_top_p = p.top_p if p.top_p is not None else 1.0 + self._editing_preset_max_output_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096 + self._editing_preset_is_new = False + finally: + imgui.end_child() + imgui.same_line() + imgui.begin_child("preset_edit_area", imgui.ImVec2(0, avail.y), False) + try: + p_name = self._editing_preset_name or "(New Preset)" + imgui.text_colored(C_IN, f"Editing Preset: {p_name}") + imgui.separator() + imgui.text("Name:") + _, self._editing_preset_name = imgui.input_text("##edit_name", self._editing_preset_name) + imgui.text("Scope:") + if imgui.radio_button("Global", self._editing_preset_scope == "global"): + self._editing_preset_scope = "global" + imgui.same_line() + if imgui.radio_button("Project", self._editing_preset_scope == "project"): + self._editing_preset_scope = "project" + imgui.text("Content:") + _, self._editing_preset_content = imgui.input_text_multiline("##edit_content", self._editing_preset_content, imgui.ImVec2(-1, 280)) + + imgui.text("Temperature:") + _, self._editing_preset_temperature = imgui.input_float("##edit_temp", self._editing_preset_temperature, 0.1, 1.0, "%.2f") + imgui.text("Top P:") + _, self._editing_preset_top_p = imgui.input_float("##edit_top_p", self._editing_preset_top_p, 0.1, 1.0, "%.2f") + imgui.text("Max Output Tokens:") + _, self._editing_preset_max_output_tokens = imgui.input_int("##edit_max_tokens", self._editing_preset_max_output_tokens) + + if imgui.button("Save", imgui.ImVec2(120, 0)): + if self._editing_preset_name.strip(): + self.controller._cb_save_preset( + self._editing_preset_name.strip(), + self._editing_preset_content, + self._editing_preset_temperature, + self._editing_preset_top_p, + self._editing_preset_max_output_tokens, + self._editing_preset_scope + ) + self.ai_status = f"Preset '{self._editing_preset_name.strip()}' saved to {self._editing_preset_scope}" + imgui.set_item_tooltip("Save the current preset settings") + imgui.same_line() + if imgui.button("Delete", imgui.ImVec2(120, 0)): + if self._editing_preset_name.strip(): + try: + self.controller._cb_delete_preset(self._editing_preset_name.strip(), self._editing_preset_scope) + self.ai_status = f"Preset '{self._editing_preset_name}' deleted from {self._editing_preset_scope}" + self._editing_preset_name = "" + self._editing_preset_content = "" + except Exception as e: + self.ai_status = f"Error deleting: {e}" + imgui.set_item_tooltip("Delete the selected preset") + imgui.same_line() + if imgui.button("Close", imgui.ImVec2(120, 0)): + self.show_preset_manager_modal = False + imgui.close_current_popup() + finally: + imgui.end_child() + finally: + imgui.end_popup() def _render_projects_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel") diff --git a/src/presets.py b/src/presets.py index ad71db0..20bbd24 100644 --- a/src/presets.py +++ b/src/presets.py @@ -84,6 +84,8 @@ class PresetManager: return {"presets": {}} def _save_file(self, path: Path, data: Dict[str, Any]) -> None: + if path.parent.exists() and path.parent.is_file(): + raise ValueError(f"Cannot save to {path}: Parent directory {path.parent} is a file. The project root seems to be a file.") path.parent.mkdir(parents=True, exist_ok=True) with open(path, "wb") as f: f.write(tomli_w.dumps(data).encode("utf-8"))