fix(conductor): Resolve FileExistsError and harden Preset Manager modal

This commit is contained in:
2026-03-09 22:59:22 -04:00
parent 451d19126f
commit 178a694e2a
7 changed files with 110 additions and 84 deletions

View File

@@ -41,6 +41,9 @@
## Phase 4: Final Integration & Polish ## Phase 4: Final Integration & Polish
- [x] Task: Ensure robust error handling for missing or malformed `.toml` files. - [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: Final UI polish (spacing, icons, tooltips).
- [x] Task: Run full suite of relevant tests. - [x] Task: Run full suite of relevant tests.
- [x] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md) - [x] Task: Conductor - User Manual Verification 'Phase 4: Final Integration & Polish' (Protocol in workflow.md)

View File

@@ -2,9 +2,10 @@
provider = "minimax" provider = "minimax"
model = "MiniMax-M2.5" model = "MiniMax-M2.5"
temperature = 0.0 temperature = 0.0
max_tokens = 24000 max_tokens = 4096
history_trunc_limit = 900000 history_trunc_limit = 900000
system_prompt = "" active_preset = "Default"
system_prompt = "Not sure yet."
[projects] [projects]
paths = [ paths = [
@@ -40,7 +41,7 @@ Response = false
"Tool Calls" = false "Tool Calls" = false
Theme = true Theme = true
"Log Management" = true "Log Management" = true
Diagnostics = true Diagnostics = false
[theme] [theme]
palette = "Nord Dark" palette = "Nord Dark"

View File

@@ -114,14 +114,14 @@ Collapsed=0
DockId=0x00000012,0 DockId=0x00000012,0
[Window][Files & Media] [Window][Files & Media]
Pos=0,1849 Pos=0,2013
Size=762,288 Size=762,124
Collapsed=0 Collapsed=0
DockId=0x00000002,0 DockId=0x00000002,0
[Window][AI Settings] [Window][AI Settings]
Pos=0,975 Pos=0,975
Size=762,872 Size=762,1036
Collapsed=0 Collapsed=0
DockId=0x00000001,0 DockId=0x00000001,0
@@ -321,6 +321,11 @@ Pos=755,679
Size=420,966 Size=420,966
Collapsed=0 Collapsed=0
[Window][Preset Manager]
Pos=786,858
Size=780,650
Collapsed=0
[Table][0xFB6E3870,4] [Table][0xFB6E3870,4]
RefScale=13 RefScale=13
Column 0 Width=80 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=0x00000007 Parent=0x0000000B SizeRef=762,858 Split=Y Selected=0x8CA2375C
DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,943 Selected=0xF4139CA2 DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,943 Selected=0xF4139CA2
DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,1168 Split=Y Selected=0x7BD57D6A 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=0x00000001 Parent=0x00000006 SizeRef=824,1036 CentralNode=1 Selected=0x7BD57D6A
DockNode ID=0x00000002 Parent=0x00000006 SizeRef=824,288 Selected=0x1DCB2623 DockNode ID=0x00000002 Parent=0x00000006 SizeRef=824,124 Selected=0x1DCB2623
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1908,858 Split=X Selected=0x418C7449 DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1908,858 Split=X Selected=0x418C7449
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=902,402 Selected=0x418C7449 DockNode ID=0x00000012 Parent=0x0000000E SizeRef=902,402 Selected=0x418C7449
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1004,402 Selected=0x6F2B5B04 DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1004,402 Selected=0x6F2B5B04

5
presets.toml Normal file
View File

@@ -0,0 +1,5 @@
[presets.Default]
system_prompt = "Not sure yet."
temperature = 0.0
top_p = 1.0
max_output_tokens = 4096

View File

@@ -822,7 +822,7 @@ class AppController:
self.ui_auto_add_history = disc_sec.get("auto_add", False) self.ui_auto_add_history = disc_sec.get("auto_add", False)
self.ui_global_system_prompt = self.config.get("ai", {}).get("system_prompt", "") 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.presets = self.preset_manager.load_all()
self.ui_global_preset_name = ai_cfg.get("active_preset") self.ui_global_preset_name = ai_cfg.get("active_preset")
self.ui_project_preset_name = proj_meta.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 self.max_tokens = preset.max_output_tokens
def _cb_save_preset(self, name, content, temp, top_p, max_tok, scope): 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( preset = models.Preset(
name=name, name=name,
system_prompt=content, system_prompt=content,

View File

@@ -409,13 +409,13 @@ class App:
if exp: if exp:
if imgui.collapsing_header("Provider & Model"): if imgui.collapsing_header("Provider & Model"):
self._render_provider_panel() self._render_provider_panel()
if imgui.collapsing_header("System Prompts"):
self._render_system_prompts_panel()
if imgui.collapsing_header("Token Budget"): if imgui.collapsing_header("Token Budget"):
self._render_token_budget_panel() self._render_token_budget_panel()
self._render_cache_panel() self._render_cache_panel()
self._render_tool_analytics_panel() self._render_tool_analytics_panel()
self._render_session_insights_panel() self._render_session_insights_panel()
if imgui.collapsing_header("System Prompts"):
self._render_system_prompts_panel()
imgui.end() imgui.end()
if self.show_windows.get("MMA Dashboard", False): if self.show_windows.get("MMA Dashboard", False):
@@ -864,81 +864,89 @@ class App:
def _render_preset_manager_modal(self) -> None: def _render_preset_manager_modal(self) -> None:
if not self.show_preset_manager_modal: return if not self.show_preset_manager_modal: return
imgui.open_popup("Preset Manager") imgui.open_popup("Preset Manager")
if imgui.begin_popup_modal("Preset Manager", True, imgui.WindowFlags_.always_auto_resize)[0]: opened, self.show_preset_manager_modal = imgui.begin_popup_modal("Preset Manager", self.show_preset_manager_modal)
imgui.begin_child("preset_list_area", imgui.ImVec2(250, 600), True) if opened:
preset_names = sorted(self.controller.presets.keys()) try:
if imgui.button("New Preset", imgui.ImVec2(-1, 0)): avail = imgui.get_content_region_avail()
self._editing_preset_name = "" imgui.begin_child("preset_list_area", imgui.ImVec2(250, avail.y), True)
self._editing_preset_content = "" try:
self._editing_preset_temperature = 0.0 preset_names = sorted(self.controller.presets.keys())
self._editing_preset_top_p = 1.0 if imgui.button("New Preset", imgui.ImVec2(-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}"
self._editing_preset_name = "" self._editing_preset_name = ""
self._editing_preset_content = "" self._editing_preset_content = ""
except Exception as e: self._editing_preset_temperature = 0.0
self.ai_status = f"Error deleting: {e}" self._editing_preset_top_p = 1.0
imgui.set_item_tooltip("Delete the selected preset") self._editing_preset_max_output_tokens = 4096
imgui.same_line() self._editing_preset_scope = "project"
if imgui.button("Close", imgui.ImVec2(120, 0)): self._editing_preset_is_new = True
self.show_preset_manager_modal = False imgui.separator()
imgui.close_current_popup() for name in preset_names:
imgui.end_child() p = self.controller.presets[name]
imgui.end_popup() 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: def _render_projects_panel(self) -> None:
if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel") if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel")

View File

@@ -84,6 +84,8 @@ class PresetManager:
return {"presets": {}} return {"presets": {}}
def _save_file(self, path: Path, data: Dict[str, Any]) -> None: 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) path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "wb") as f: with open(path, "wb") as f:
f.write(tomli_w.dumps(data).encode("utf-8")) f.write(tomli_w.dumps(data).encode("utf-8"))