From 6da3d95c0ec117cfa4cc3ac523b7b1b244d77186 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 10 Mar 2026 23:21:14 -0400 Subject: [PATCH] refactor(gui): redesign persona editor UI and replace popup modals with standard windows --- src/app_controller.py | 13 +- src/gui_2.py | 1059 +++++++++++++++++---------------- src/models.py | 81 ++- tests/test_persona_manager.py | 2 +- tests/test_persona_models.py | 36 +- 5 files changed, 654 insertions(+), 537 deletions(-) diff --git a/src/app_controller.py b/src/app_controller.py index 32b6c8f..b4ad96e 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -300,7 +300,9 @@ class AppController: self._inject_mode: str = "skeleton" self._inject_preview: str = "" self._show_inject_modal: bool = False - self.show_preset_manager_modal: bool = False + self.show_preset_manager_window: bool = False + self.show_tool_preset_manager_window: bool = False + self.show_persona_editor_window: bool = False self._editing_preset_name: str = "" self._editing_preset_content: str = "" self._editing_preset_temperature: float = 0.0 @@ -342,7 +344,9 @@ class AppController: 'ui_active_tool_preset': 'ui_active_tool_preset', 'temperature': 'temperature', 'max_tokens': 'max_tokens', - 'show_preset_manager_modal': 'show_preset_manager_modal', + 'show_preset_manager_window': 'show_preset_manager_window', + 'show_tool_preset_manager_window': 'show_tool_preset_manager_window', + 'show_persona_editor_window': 'show_persona_editor_window', '_editing_preset_name': '_editing_preset_name', '_editing_preset_content': '_editing_preset_content', '_editing_preset_temperature': '_editing_preset_temperature', @@ -390,7 +394,9 @@ class AppController: 'ui_active_tool_preset': 'ui_active_tool_preset', 'temperature': 'temperature', 'max_tokens': 'max_tokens', - 'show_preset_manager_modal': 'show_preset_manager_modal', + 'show_preset_manager_window': 'show_preset_manager_window', + 'show_tool_preset_manager_window': 'show_tool_preset_manager_window', + 'show_persona_editor_window': 'show_persona_editor_window', '_editing_preset_name': '_editing_preset_name', '_editing_preset_content': '_editing_preset_content', '_editing_preset_temperature': '_editing_preset_temperature', @@ -2567,3 +2573,4 @@ class AppController: tasks=self.active_track.tickets ) project_manager.save_track_state(self.active_track.id, state, self.ui_files_base_dir) + diff --git a/src/gui_2.py b/src/gui_2.py index 9f09d38..a1f6772 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -96,9 +96,9 @@ class App: self.controller.init_state() self.show_windows.setdefault("Diagnostics", False) self.controller.start_services(self) - self.show_preset_manager_modal = False - self.show_tool_preset_manager_modal = False - self.show_persona_editor_modal = False + self.show_preset_manager_window = False + self.show_tool_preset_manager_window = False + self.show_persona_editor_window = False self.ui_active_tool_preset = "" self.ui_active_bias_profile = "" self.ui_active_persona = "" @@ -111,7 +111,7 @@ class App: self._editing_persona_max_tokens = 4096 self._editing_persona_tool_preset_id = "" self._editing_persona_bias_profile_id = "" - self._editing_persona_preferred_models_list: list[str] = [] + self._editing_persona_preferred_models_list: list[dict] = [] self._editing_persona_scope = "project" self._editing_persona_is_new = True self._persona_editor_opened = False @@ -384,9 +384,9 @@ class App: self._render_track_proposal_modal() self._render_patch_modal() self._render_save_preset_modal() - self._render_preset_manager_modal() - self._render_tool_preset_manager_modal() - self._render_persona_editor_modal() + self._render_preset_manager_window() + self._render_tool_preset_manager_window() + self._render_persona_editor_window() # Auto-save (every 60s) now = time.time() if now - self._last_autosave >= self._autosave_interval: @@ -923,513 +923,577 @@ class App: imgui.close_current_popup() imgui.end_popup() - def _render_preset_manager_modal(self) -> None: - if not self.show_preset_manager_modal: return - imgui.open_popup("Preset Manager") - opened, self.show_preset_manager_modal = imgui.begin_popup_modal("Preset Manager", self.show_preset_manager_modal) - if opened: + def _render_preset_manager_window(self, is_embedded: bool = False) -> None: + if not self.show_preset_manager_window and not is_embedded: return + + if not is_embedded: + imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever) + opened, self.show_preset_manager_window = imgui.begin("Preset Manager", self.show_preset_manager_window) + if not opened: + imgui.end() + return + + try: + avail = imgui.get_content_region_avail() + imgui.begin_child("preset_list_area", imgui.ImVec2(250, avail.y), True) 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 = "" - 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_is_new = False - finally: - imgui.end_child() + 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_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_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() - 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, -40)) + 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, -40)) - 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_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") + 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_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") + if not is_embedded: 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() + self.show_preset_manager_window = False finally: - imgui.end_popup() + imgui.end_child() + finally: + if not is_embedded: + imgui.end() - def _render_tool_preset_manager_modal(self) -> None: - if not self.show_tool_preset_manager_modal: return - imgui.open_popup("Tool Preset Manager") - opened, self.show_tool_preset_manager_modal = imgui.begin_popup_modal("Tool Preset Manager", self.show_tool_preset_manager_modal) - if opened: + def _render_tool_preset_manager_window(self, is_embedded: bool = False) -> None: + if not self.show_tool_preset_manager_window and not is_embedded: return + + if not is_embedded: + imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) + opened, self.show_tool_preset_manager_window = imgui.begin("Tool Preset Manager", self.show_tool_preset_manager_window) + if not opened: + imgui.end() + return + + try: + avail = imgui.get_content_region_avail() + # Left Column: Listbox + imgui.begin_child("tool_preset_list_area", imgui.ImVec2(250, avail.y), True) try: - avail = imgui.get_content_region_avail() - # Left Column: Listbox - imgui.begin_child("tool_preset_list_area", imgui.ImVec2(250, avail.y), True) - try: - if imgui.button("New Tool Preset", imgui.ImVec2(-1, 0)): - self._editing_tool_preset_name = "" + if imgui.button("New Tool Preset", imgui.ImVec2(-1, 0)): + self._editing_tool_preset_name = "" + self._editing_tool_preset_categories = {cat: {} for cat in models.DEFAULT_TOOL_CATEGORIES} + self._editing_tool_preset_scope = "project" + self._selected_tool_preset_idx = -1 + if imgui.is_item_hovered(): + imgui.set_tooltip("Create a new tool preset configuration.") + + imgui.separator() + preset_names = sorted(self.controller.tool_presets.keys()) + for i, name in enumerate(preset_names): + is_selected = (self._selected_tool_preset_idx == i) + if imgui.selectable(name, is_selected)[0]: + self._selected_tool_preset_idx = i + self._editing_tool_preset_name = name + preset = self.controller.tool_presets[name] self._editing_tool_preset_categories = {cat: {} for cat in models.DEFAULT_TOOL_CATEGORIES} - self._editing_tool_preset_scope = "project" - self._selected_tool_preset_idx = -1 - if imgui.is_item_hovered(): - imgui.set_tooltip("Create a new tool preset configuration.") - - imgui.separator() - preset_names = sorted(self.controller.tool_presets.keys()) - for i, name in enumerate(preset_names): - is_selected = (self._selected_tool_preset_idx == i) - if imgui.selectable(name, is_selected)[0]: - self._selected_tool_preset_idx = i - self._editing_tool_preset_name = name - preset = self.controller.tool_presets[name] - self._editing_tool_preset_categories = {cat: {} for cat in models.DEFAULT_TOOL_CATEGORIES} - for cat, tools in preset.categories.items(): - self._editing_tool_preset_categories[cat] = copy.deepcopy(tools) - finally: - imgui.end_child() - - imgui.same_line() - - # Right Column: Edit Area - imgui.begin_child("tool_preset_edit_area", imgui.ImVec2(0, avail.y), False) - try: - p_name = self._editing_tool_preset_name or "(New Tool Preset)" - imgui.text_colored(C_IN, f"Editing Tool Preset: {p_name}") - imgui.separator() - imgui.dummy(imgui.ImVec2(0, 8)) - - imgui.text("Name:") - _, self._editing_tool_preset_name = imgui.input_text("##edit_tp_name", self._editing_tool_preset_name) - imgui.dummy(imgui.ImVec2(0, 8)) - - imgui.text("Scope:") - if imgui.radio_button("Global", self._editing_tool_preset_scope == "global"): - self._editing_tool_preset_scope = "global" - imgui.same_line() - if imgui.radio_button("Project", self._editing_tool_preset_scope == "project"): - self._editing_tool_preset_scope = "project" - imgui.dummy(imgui.ImVec2(0, 8)) - - imgui.text("Categories & Tools:") - imgui.begin_child("tp_categories_scroll", imgui.ImVec2(0, 300), True) - try: - for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): - if imgui.tree_node(cat_name): - if cat_name not in self._editing_tool_preset_categories: - self._editing_tool_preset_categories[cat_name] = [] - current_cat_tools = self._editing_tool_preset_categories[cat_name] # List of Tool - - for tool_name in default_tools: - # Find existing Tool object in list - tool = next((t for t in current_cat_tools if t.name == tool_name), None) - mode = "disabled" if tool is None else tool.approval - - if imgui.radio_button(f"Off##{cat_name}_{tool_name}", mode == "disabled"): - if tool: current_cat_tools.remove(tool) - imgui.same_line() - if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"): - if not tool: - tool = models.Tool(name=tool_name, approval="auto") - current_cat_tools.append(tool) - else: - tool.approval = "auto" - imgui.same_line() - if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"): - if not tool: - tool = models.Tool(name=tool_name, approval="ask") - current_cat_tools.append(tool) - else: - tool.approval = "ask" - imgui.same_line() - imgui.text(tool_name) - - if tool: - imgui.same_line(250) - imgui.set_next_item_width(100) - _, tool.weight = imgui.slider_int(f"Weight##{cat_name}_{tool_name}", tool.weight, 1, 5) - imgui.same_line() - pb_str = json.dumps(tool.parameter_bias) - imgui.set_next_item_width(150) - ch_pb, pb_new = imgui.input_text(f"Params##{cat_name}_{tool_name}", pb_str) - if ch_pb: - try: tool.parameter_bias = json.loads(pb_new) - except: pass - imgui.tree_pop() - finally: - imgui.end_child() - - imgui.separator() - imgui.text_colored(C_SUB, "Bias Profiles") - imgui.begin_child("bias_profiles_area", imgui.ImVec2(0, 300), True) - try: - avail_bias = imgui.get_content_region_avail() - imgui.begin_child("bias_list", imgui.ImVec2(200, avail_bias.y), False) - if imgui.button("New Profile", imgui.ImVec2(-1, 0)): - self._editing_bias_profile_name = "" - self._editing_bias_profile_tool_weights = {} - self._editing_bias_profile_category_multipliers = {} - self._selected_bias_profile_idx = -1 - imgui.separator() - bnames = sorted(self.bias_profiles.keys()) - for i, bname in enumerate(bnames): - is_sel = (self._selected_bias_profile_idx == i) - if imgui.selectable(bname, is_sel)[0]: - self._selected_bias_profile_idx = i - self._editing_bias_profile_name = bname - profile = self.bias_profiles[bname] - self._editing_bias_profile_tool_weights = copy.deepcopy(profile.tool_weights) - self._editing_bias_profile_category_multipliers = copy.deepcopy(profile.category_multipliers) - imgui.end_child() - imgui.same_line() - imgui.begin_child("bias_edit", imgui.ImVec2(0, avail_bias.y), False) - imgui.text("Name:") - _, self._editing_bias_profile_name = imgui.input_text("##b_name", self._editing_bias_profile_name) - - imgui.text_colored(C_KEY, "Tool Weights:") - to_remove_tw = [] - for tw_name, tw_val in list(self._editing_bias_profile_tool_weights.items()): - imgui.text(f" {tw_name}:") - imgui.same_line(150) - imgui.set_next_item_width(100) - changed, new_val = imgui.slider_int(f"##tw_{tw_name}", tw_val, 1, 10) - if changed: self._editing_bias_profile_tool_weights[tw_name] = new_val - imgui.same_line() - if imgui.button(f"x##rem_tw_{tw_name}"): - to_remove_tw.append(tw_name) - for r in to_remove_tw: del self._editing_bias_profile_tool_weights[r] - - # Add Tool Override - imgui.set_next_item_width(150) - if imgui.begin_combo("##add_tw_combo", self._new_bias_tool_name): - for tn in models.AGENT_TOOL_NAMES: - if tn not in self._editing_bias_profile_tool_weights: - if imgui.selectable(tn, tn == self._new_bias_tool_name)[0]: - self._new_bias_tool_name = tn - imgui.end_combo() - imgui.same_line() - if imgui.button("Add Tool Override"): - self._editing_bias_profile_tool_weights[self._new_bias_tool_name] = 5 - - imgui.dummy(imgui.ImVec2(0, 4)) - imgui.text_colored(C_KEY, "Category Multipliers:") - to_remove_cm = [] - for cm_name, cm_val in list(self._editing_bias_profile_category_multipliers.items()): - imgui.text(f" {cm_name}:") - imgui.same_line(150) - imgui.set_next_item_width(100) - changed, new_val = imgui.slider_float(f"##cm_{cm_name}", cm_val, 0.1, 5.0, "%.1fx") - if changed: self._editing_bias_profile_category_multipliers[cm_name] = new_val - imgui.same_line() - if imgui.button(f"x##rem_cm_{cm_name}"): - to_remove_cm.append(cm_name) - for r in to_remove_cm: del self._editing_bias_profile_category_multipliers[r] - - # Add Category Override - imgui.set_next_item_width(150) - cat_names = sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) - if imgui.begin_combo("##add_cm_combo", self._new_bias_category_name): - for cn in cat_names: - if cn not in self._editing_bias_profile_category_multipliers: - if imgui.selectable(cn, cn == self._new_bias_category_name)[0]: - self._new_bias_category_name = cn - imgui.end_combo() - imgui.same_line() - if imgui.button("Add Category Override"): - self._editing_bias_profile_category_multipliers[self._new_bias_category_name] = 1.0 - - imgui.dummy(imgui.ImVec2(0, 8)) - if imgui.button("Save Profile"): - try: - prof = models.BiasProfile( - name=self._editing_bias_profile_name, - tool_weights=self._editing_bias_profile_tool_weights, - category_multipliers=self._editing_bias_profile_category_multipliers - ) - self.controller._cb_save_bias_profile(prof, self._editing_tool_preset_scope) - self.ai_status = f"Bias profile '{prof.name}' saved" - except Exception as e: - self.ai_status = f"Error: {e}" - imgui.same_line() - if imgui.button("Delete Profile"): - self.controller._cb_delete_bias_profile(self._editing_bias_profile_name, self._editing_tool_preset_scope) - self.ai_status = f"Bias profile deleted" - imgui.end_child() - finally: - imgui.end_child() - - imgui.dummy(imgui.ImVec2(0, 8)) - if imgui.button("Save", imgui.ImVec2(100, 0)): - if self._editing_tool_preset_name.strip(): - self.controller._cb_save_tool_preset( - self._editing_tool_preset_name.strip(), - self._editing_tool_preset_categories, - self._editing_tool_preset_scope - ) - self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' saved" - if imgui.is_item_hovered(): - imgui.set_tooltip("Save the current tool preset configuration.") - - imgui.same_line() - if imgui.button("Delete", imgui.ImVec2(100, 0)): - if self._editing_tool_preset_name.strip(): - self.controller._cb_delete_tool_preset( - self._editing_tool_preset_name.strip(), - self._editing_tool_preset_scope - ) - self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' deleted" - self._editing_tool_preset_name = "" - self._editing_tool_preset_categories = {} - self._selected_tool_preset_idx = -1 - if imgui.is_item_hovered(): - imgui.set_tooltip("Delete this tool preset permanently.") - - imgui.same_line() - if imgui.button("Close", imgui.ImVec2(100, 0)): - self.show_tool_preset_manager_modal = False - imgui.close_current_popup() - finally: - imgui.end_child() + for cat, tools in preset.categories.items(): + self._editing_tool_preset_categories[cat] = copy.deepcopy(tools) finally: - imgui.end_popup() + imgui.end_child() - def _render_persona_editor_modal(self) -> None: - if not self.show_persona_editor_modal: return - imgui.open_popup("Persona Editor") - imgui.set_next_window_size(imgui.ImVec2(800, 600), imgui.Cond_.first_use_ever) - opened, self.show_persona_editor_modal = imgui.begin_popup_modal("Persona Editor", self.show_persona_editor_modal) - if opened: + imgui.same_line() + + # Right Column: Edit Area + imgui.begin_child("tool_preset_edit_area", imgui.ImVec2(0, avail.y), False) try: - avail = imgui.get_content_region_avail() - # Left Pane: List of Personas - imgui.begin_child("persona_list_area", imgui.ImVec2(250, avail.y), True) + p_name = self._editing_tool_preset_name or "(New Tool Preset)" + imgui.text_colored(C_IN, f"Editing Tool Preset: {p_name}") + imgui.separator() + imgui.dummy(imgui.ImVec2(0, 8)) + + imgui.text("Name:") + _, self._editing_tool_preset_name = imgui.input_text("##edit_tp_name", self._editing_tool_preset_name) + imgui.dummy(imgui.ImVec2(0, 8)) + + imgui.text("Scope:") + if imgui.radio_button("Global", self._editing_tool_preset_scope == "global"): + self._editing_tool_preset_scope = "global" + imgui.same_line() + if imgui.radio_button("Project", self._editing_tool_preset_scope == "project"): + self._editing_tool_preset_scope = "project" + imgui.dummy(imgui.ImVec2(0, 8)) + + imgui.text("Categories & Tools:") + imgui.begin_child("tp_categories_scroll", imgui.ImVec2(0, 300), True) try: - if imgui.button("New Persona", imgui.ImVec2(-1, 0)): - self._editing_persona_name = "" - self._editing_persona_provider = self.current_provider - self._editing_persona_model = self.current_model - self._editing_persona_system_prompt = "" - self._editing_persona_temperature = 0.7 - self._editing_persona_max_tokens = 4096 - self._editing_persona_tool_preset_id = "" - self._editing_persona_bias_profile_id = "" - self._editing_persona_preferred_models_list = [] - self._editing_persona_scope = "project" - self._editing_persona_is_new = True - imgui.separator() - personas = getattr(self.controller, 'personas', {}) - for name in sorted(personas.keys()): - is_sel = (name == self._editing_persona_name and not self._editing_persona_is_new) - if imgui.selectable(name, is_sel)[0]: - p = personas[name] - self._editing_persona_name = p.name - self._editing_persona_provider = p.provider or "" - self._editing_persona_model = p.model or "" - self._editing_persona_system_prompt = p.system_prompt or "" - self._editing_persona_temperature = p.temperature if p.temperature is not None else 0.7 - self._editing_persona_max_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096 - self._editing_persona_tool_preset_id = p.tool_preset or "" - self._editing_persona_bias_profile_id = p.bias_profile or "" - self._editing_persona_preferred_models_list = list(p.preferred_models) if p.preferred_models else [] - self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name) - self._editing_persona_is_new = False + for cat_name, default_tools in models.DEFAULT_TOOL_CATEGORIES.items(): + if imgui.tree_node(cat_name): + if cat_name not in self._editing_tool_preset_categories: + self._editing_tool_preset_categories[cat_name] = [] + current_cat_tools = self._editing_tool_preset_categories[cat_name] # List of Tool + + for tool_name in default_tools: + # Find existing Tool object in list + tool = next((t for t in current_cat_tools if t.name == tool_name), None) + mode = "disabled" if tool is None else tool.approval + + if imgui.radio_button(f"Off##{cat_name}_{tool_name}", mode == "disabled"): + if tool: current_cat_tools.remove(tool) + imgui.same_line() + if imgui.radio_button(f"Auto##{cat_name}_{tool_name}", mode == "auto"): + if not tool: + tool = models.Tool(name=tool_name, approval="auto") + current_cat_tools.append(tool) + else: + tool.approval = "auto" + imgui.same_line() + if imgui.radio_button(f"Ask##{cat_name}_{tool_name}", mode == "ask"): + if not tool: + tool = models.Tool(name=tool_name, approval="ask") + current_cat_tools.append(tool) + else: + tool.approval = "ask" + imgui.same_line() + imgui.text(tool_name) + + if tool: + imgui.same_line(250) + imgui.set_next_item_width(100) + _, tool.weight = imgui.slider_int(f"Weight##{cat_name}_{tool_name}", tool.weight, 1, 5) + imgui.same_line() + pb_str = json.dumps(tool.parameter_bias) + imgui.set_next_item_width(150) + ch_pb, pb_new = imgui.input_text(f"Params##{cat_name}_{tool_name}", pb_str) + if ch_pb: + try: tool.parameter_bias = json.loads(pb_new) + except: pass + imgui.tree_pop() finally: imgui.end_child() - - imgui.same_line() - - # Right Pane: Editor - imgui.begin_child("persona_edit_area", imgui.ImVec2(0, avail.y), False) - try: - header = "New Persona" if self._editing_persona_is_new else f"Editing Persona: {self._editing_persona_name}" - imgui.text_colored(C_IN, header) - imgui.separator() - - imgui.text("Name:") - imgui.same_line() - _, self._editing_persona_name = imgui.input_text("##pname", self._editing_persona_name, 128) - - imgui.text("Scope:") - if imgui.radio_button("Global##pscope", self._editing_persona_scope == "global"): - self._editing_persona_scope = "global" - imgui.same_line() - if imgui.radio_button("Project##pscope", self._editing_persona_scope == "project"): - self._editing_persona_scope = "project" - - imgui.separator() - - imgui.text("Provider:") - imgui.same_line() - providers = self.controller.PROVIDERS - p_idx = providers.index(self._editing_persona_provider) + 1 if self._editing_persona_provider in providers else 0 - imgui.push_item_width(150) - _, p_idx = imgui.combo("##pprov", p_idx, ["None"] + providers) - self._editing_persona_provider = providers[p_idx - 1] if p_idx > 0 else "" - imgui.pop_item_width() - - imgui.text("Model:") - imgui.same_line() - all_models = self.controller.all_available_models.get(self._editing_persona_provider, []) - if not all_models and self._editing_persona_model: - all_models = [self._editing_persona_model] - m_idx = all_models.index(self._editing_persona_model) + 1 if self._editing_persona_model in all_models else 0 - imgui.push_item_width(200) - _, m_idx = imgui.combo("##pmodel", m_idx, ["None"] + all_models) - self._editing_persona_model = all_models[m_idx - 1] if m_idx > 0 else "" - imgui.pop_item_width() - - imgui.text("Temp:") - imgui.same_line() - _, self._editing_persona_temperature = imgui.slider_float("##ptemp", self._editing_persona_temperature, 0.0, 2.0) - - imgui.text("Max Output Tokens:") - imgui.same_line() - _, self._editing_persona_max_tokens = imgui.input_int("##pmaxt", self._editing_persona_max_tokens) - - imgui.text("Tool Preset:") - imgui.same_line() - t_preset_names = ["None"] + sorted(self.controller.tool_presets.keys()) - t_idx = t_preset_names.index(self._editing_persona_tool_preset_id) if self._editing_persona_tool_preset_id in t_preset_names else 0 - imgui.push_item_width(200) - _, t_idx = imgui.combo("##ptoolpreset", t_idx, t_preset_names) - self._editing_persona_tool_preset_id = t_preset_names[t_idx] if t_idx > 0 else "" - imgui.pop_item_width() - - imgui.text("Bias Profile:") - imgui.same_line() - bias_names = ["None"] + sorted(self.controller.bias_profiles.keys()) - b_idx = bias_names.index(self._editing_persona_bias_profile_id) if self._editing_persona_bias_profile_id in bias_names else 0 - imgui.push_item_width(200) - _, b_idx = imgui.combo("##pbiasprofile", b_idx, bias_names) - self._editing_persona_bias_profile_id = bias_names[b_idx] if b_idx > 0 else "" - imgui.pop_item_width() - - imgui.separator() - imgui.text("Preferred Models:") - to_remove = [] - for i, model in enumerate(self._editing_persona_preferred_models_list): - imgui.text(f"- {model}") - imgui.same_line() - if imgui.button(f"x##pref_rem_{i}"): - to_remove.append(i) - for i in reversed(to_remove): - self._editing_persona_preferred_models_list.pop(i) - - # Add Preferred Model - all_possible_models = [] - for prov_models in self.controller.all_available_models.values(): - all_possible_models.extend(prov_models) - all_possible_models = sorted(list(set(all_possible_models))) - - if not hasattr(self, "_add_pref_model_idx"): self._add_pref_model_idx = 0 - imgui.push_item_width(200) - _, self._add_pref_model_idx = imgui.combo("Add Preferred Model##add_pref", self._add_pref_model_idx, ["Select Model..."] + all_possible_models) - imgui.pop_item_width() - if self._add_pref_model_idx > 0: - new_m = all_possible_models[self._add_pref_model_idx - 1] - if new_m not in self._editing_persona_preferred_models_list: - self._editing_persona_preferred_models_list.append(new_m) - self._add_pref_model_idx = 0 - - imgui.separator() - imgui.text("System Prompt:") - - # Load Prompt Preset - imgui.text("Load from Preset:") - imgui.same_line() - prompt_presets = ["Select..."] + sorted(self.controller.presets.keys()) - if not hasattr(self, "_load_preset_idx"): self._load_preset_idx = 0 - imgui.push_item_width(150) - _, self._load_preset_idx = imgui.combo("##load_preset", self._load_preset_idx, prompt_presets) - imgui.pop_item_width() - imgui.same_line() - if imgui.button("Apply##apply_p"): - if self._load_preset_idx > 0: - pname = prompt_presets[self._load_preset_idx] - if pname in self.controller.presets: - p = self.controller.presets[pname] - self._editing_persona_system_prompt = p.system_prompt - if p.temperature is not None: self._editing_persona_temperature = p.temperature - if p.max_output_tokens is not None: self._editing_persona_max_tokens = p.max_output_tokens - self._load_preset_idx = 0 - _, self._editing_persona_system_prompt = imgui.input_text_multiline("##pprompt", self._editing_persona_system_prompt, imgui.ImVec2(-1, 150)) - + imgui.separator() + imgui.text_colored(C_SUB, "Bias Profiles") + imgui.begin_child("bias_profiles_area", imgui.ImVec2(0, 300), True) + try: + avail_bias = imgui.get_content_region_avail() + imgui.begin_child("bias_list", imgui.ImVec2(200, avail_bias.y), False) + if imgui.button("New Profile", imgui.ImVec2(-1, 0)): + self._editing_bias_profile_name = "" + self._editing_bias_profile_tool_weights = {} + self._editing_bias_profile_category_multipliers = {} + self._selected_bias_profile_idx = -1 imgui.separator() - if imgui.button("Save Persona", imgui.ImVec2(120, 0)): - if self._editing_persona_name.strip(): - try: - persona = models.Persona( - name=self._editing_persona_name.strip(), - provider=self._editing_persona_provider or None, - model=self._editing_persona_model or None, - system_prompt=self._editing_persona_system_prompt, - temperature=self._editing_persona_temperature, - max_output_tokens=self._editing_persona_max_tokens, - tool_preset=self._editing_persona_tool_preset_id or None, - bias_profile=self._editing_persona_bias_profile_id or None, - preferred_models=self._editing_persona_preferred_models_list, - ) - self.controller._cb_save_persona(persona, self._editing_persona_scope) - self.ai_status = f"Saved Persona: {persona.name}" - except Exception as e: - self.ai_status = f"Error saving persona: {e}" - else: - self.ai_status = "Name required" - + bnames = sorted(self.bias_profiles.keys()) + for i, bname in enumerate(bnames): + is_sel = (self._selected_bias_profile_idx == i) + if imgui.selectable(bname, is_sel)[0]: + self._selected_bias_profile_idx = i + self._editing_bias_profile_name = bname + profile = self.bias_profiles[bname] + self._editing_bias_profile_tool_weights = copy.deepcopy(profile.tool_weights) + self._editing_bias_profile_category_multipliers = copy.deepcopy(profile.category_multipliers) + imgui.end_child() imgui.same_line() - if imgui.button("Delete", imgui.ImVec2(100, 0)): - if not self._editing_persona_is_new and self._editing_persona_name: - self.controller._cb_delete_persona(self._editing_persona_name, self._editing_persona_scope) - self.ai_status = f"Deleted Persona: {self._editing_persona_name}" - self._editing_persona_name = "" - self._editing_persona_is_new = True - + imgui.begin_child("bias_edit", imgui.ImVec2(0, avail_bias.y), False) + imgui.text("Name:") + _, self._editing_bias_profile_name = imgui.input_text("##b_name", self._editing_bias_profile_name) + + imgui.text_colored(C_KEY, "Tool Weights:") + to_remove_tw = [] + for tw_name, tw_val in list(self._editing_bias_profile_tool_weights.items()): + imgui.text(f" {tw_name}:") + imgui.same_line(150) + imgui.set_next_item_width(100) + changed, new_val = imgui.slider_int(f"##tw_{tw_name}", tw_val, 1, 10) + if changed: self._editing_bias_profile_tool_weights[tw_name] = new_val + imgui.same_line() + if imgui.button(f"x##rem_tw_{tw_name}"): + to_remove_tw.append(tw_name) + for r in to_remove_tw: del self._editing_bias_profile_tool_weights[r] + + # Add Tool Override + imgui.set_next_item_width(150) + if imgui.begin_combo("##add_tw_combo", self._new_bias_tool_name): + for tn in models.AGENT_TOOL_NAMES: + if tn not in self._editing_bias_profile_tool_weights: + if imgui.selectable(tn, tn == self._new_bias_tool_name)[0]: + self._new_bias_tool_name = tn + imgui.end_combo() + imgui.same_line() + if imgui.button("Add Tool Override"): + self._editing_bias_profile_tool_weights[self._new_bias_tool_name] = 5 + + imgui.dummy(imgui.ImVec2(0, 4)) + imgui.text_colored(C_KEY, "Category Multipliers:") + to_remove_cm = [] + for cm_name, cm_val in list(self._editing_bias_profile_category_multipliers.items()): + imgui.text(f" {cm_name}:") + imgui.same_line(150) + imgui.set_next_item_width(100) + changed, new_val = imgui.slider_float(f"##cm_{cm_name}", cm_val, 0.1, 5.0, "%.1fx") + if changed: self._editing_bias_profile_category_multipliers[cm_name] = new_val + imgui.same_line() + if imgui.button(f"x##rem_cm_{cm_name}"): + to_remove_cm.append(cm_name) + for r in to_remove_cm: del self._editing_bias_profile_category_multipliers[r] + + # Add Category Override + imgui.set_next_item_width(150) + cat_names = sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) + if imgui.begin_combo("##add_cm_combo", self._new_bias_category_name): + for cn in cat_names: + if cn not in self._editing_bias_profile_category_multipliers: + if imgui.selectable(cn, cn == self._new_bias_category_name)[0]: + self._new_bias_category_name = cn + imgui.end_combo() + imgui.same_line() + if imgui.button("Add Category Override"): + self._editing_bias_profile_category_multipliers[self._new_bias_category_name] = 1.0 + + imgui.dummy(imgui.ImVec2(0, 8)) + if imgui.button("Save Profile"): + try: + prof = models.BiasProfile( + name=self._editing_bias_profile_name, + tool_weights=self._editing_bias_profile_tool_weights, + category_multipliers=self._editing_bias_profile_category_multipliers + ) + self.controller._cb_save_bias_profile(prof, self._editing_tool_preset_scope) + self.ai_status = f"Bias profile '{prof.name}' saved" + except Exception as e: + self.ai_status = f"Error: {e}" + imgui.same_line() + if imgui.button("Delete Profile"): + self.controller._cb_delete_bias_profile(self._editing_bias_profile_name, self._editing_tool_preset_scope) + self.ai_status = f"Bias profile deleted" + imgui.end_child() + finally: + imgui.end_child() + + imgui.dummy(imgui.ImVec2(0, 8)) + if imgui.button("Save", imgui.ImVec2(100, 0)): + if self._editing_tool_preset_name.strip(): + self.controller._cb_save_tool_preset( + self._editing_tool_preset_name.strip(), + self._editing_tool_preset_categories, + self._editing_tool_preset_scope + ) + self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' saved" + if imgui.is_item_hovered(): + imgui.set_tooltip("Save the current tool preset configuration.") + + imgui.same_line() + if imgui.button("Delete", imgui.ImVec2(100, 0)): + if self._editing_tool_preset_name.strip(): + self.controller._cb_delete_tool_preset( + self._editing_tool_preset_name.strip(), + self._editing_tool_preset_scope + ) + self.ai_status = f"Tool preset '{self._editing_tool_preset_name}' deleted" + self._editing_tool_preset_name = "" + self._editing_tool_preset_categories = {} + self._selected_tool_preset_idx = -1 + if imgui.is_item_hovered(): + imgui.set_tooltip("Delete this tool preset permanently.") + + if not is_embedded: imgui.same_line() if imgui.button("Close", imgui.ImVec2(100, 0)): - self.show_persona_editor_modal = False - imgui.close_current_popup() - finally: - imgui.end_child() + self.show_tool_preset_manager_window = False finally: - imgui.end_popup() + imgui.end_child() + finally: + if not is_embedded: + imgui.end() + def _render_persona_editor_window(self, is_embedded: bool = False) -> None: + if not self.show_persona_editor_window and not is_embedded: return + + if not is_embedded: + imgui.set_next_window_size(imgui.ImVec2(1000, 800), imgui.Cond_.first_use_ever) + opened, self.show_persona_editor_window = imgui.begin("Persona Editor", self.show_persona_editor_window) + if not opened: + imgui.end() + return + + try: + avail = imgui.get_content_region_avail() + # Left Pane: List of Personas + imgui.begin_child("persona_list_area", imgui.ImVec2(250, avail.y), True) + try: + if imgui.button("New Persona", imgui.ImVec2(-1, 0)): + self._editing_persona_name = "" + self._editing_persona_provider = self.current_provider + self._editing_persona_model = self.current_model + self._editing_persona_system_prompt = "" + self._editing_persona_temperature = 0.7 + self._editing_persona_max_tokens = 4096 + self._editing_persona_tool_preset_id = "" + self._editing_persona_bias_profile_id = "" + self._editing_persona_preferred_models_list = [] + self._editing_persona_scope = "project" + self._editing_persona_is_new = True + imgui.separator() + personas = getattr(self.controller, 'personas', {}) + for name in sorted(personas.keys()): + is_sel = (name == self._editing_persona_name and not self._editing_persona_is_new) + if imgui.selectable(name, is_sel)[0]: + p = personas[name] + self._editing_persona_name = p.name + self._editing_persona_provider = p.provider or "" + self._editing_persona_model = p.model or "" + self._editing_persona_system_prompt = p.system_prompt or "" + self._editing_persona_temperature = p.temperature if p.temperature is not None else 0.7 + self._editing_persona_max_tokens = p.max_output_tokens if p.max_output_tokens is not None else 4096 + self._editing_persona_tool_preset_id = p.tool_preset or "" + self._editing_persona_bias_profile_id = p.bias_profile or "" + self._editing_persona_preferred_models_list = copy.deepcopy(p.preferred_models) if p.preferred_models else [] + self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(p.name) + self._editing_persona_is_new = False + finally: + imgui.end_child() + + imgui.same_line() + + # Right Pane: Editor + imgui.begin_child("persona_edit_area", imgui.ImVec2(0, avail.y), False) + try: + header = "New Persona" if self._editing_persona_is_new else f"Editing Persona: {self._editing_persona_name}" + imgui.text_colored(C_IN, header) + imgui.separator() + + imgui.text("Name:") + imgui.same_line() + _, self._editing_persona_name = imgui.input_text("##pname", self._editing_persona_name, 128) + + imgui.text("Scope:") + if imgui.radio_button("Global##pscope", self._editing_persona_scope == "global"): + self._editing_persona_scope = "global" + imgui.same_line() + if imgui.radio_button("Project##pscope", self._editing_persona_scope == "project"): + self._editing_persona_scope = "project" + + imgui.separator() + + imgui.text("Default Provider/Model (used if Preferred Models list is empty):") + imgui.text("Provider:") + imgui.same_line() + providers = self.controller.PROVIDERS + p_idx = providers.index(self._editing_persona_provider) + 1 if self._editing_persona_provider in providers else 0 + imgui.push_item_width(150) + _, p_idx = imgui.combo("##pprov", p_idx, ["None"] + providers) + self._editing_persona_provider = providers[p_idx - 1] if p_idx > 0 else "" + imgui.pop_item_width() + + imgui.same_line() + imgui.text("Model:") + imgui.same_line() + all_models = self.controller.all_available_models.get(self._editing_persona_provider, []) + if not all_models and self._editing_persona_model: + all_models = [self._editing_persona_model] + m_idx = all_models.index(self._editing_persona_model) + 1 if self._editing_persona_model in all_models else 0 + imgui.push_item_width(200) + _, m_idx = imgui.combo("##pmodel", m_idx, ["None"] + all_models) + self._editing_persona_model = all_models[m_idx - 1] if m_idx > 0 else "" + imgui.pop_item_width() + + imgui.text("Temp:") + imgui.same_line() + imgui.set_next_item_width(100) + _, self._editing_persona_temperature = imgui.slider_float("##ptemp", self._editing_persona_temperature, 0.0, 2.0) + + imgui.same_line() + imgui.text("Max Tok:") + imgui.same_line() + imgui.set_next_item_width(100) + _, self._editing_persona_max_tokens = imgui.input_int("##pmaxt", self._editing_persona_max_tokens) + + imgui.separator() + imgui.text("Tool Preset:") + imgui.same_line() + t_preset_names = ["None"] + sorted(self.controller.tool_presets.keys()) + t_idx = t_preset_names.index(self._editing_persona_tool_preset_id) if self._editing_persona_tool_preset_id in t_preset_names else 0 + imgui.push_item_width(200) + _, t_idx = imgui.combo("##ptoolpreset", t_idx, t_preset_names) + self._editing_persona_tool_preset_id = t_preset_names[t_idx] if t_idx > 0 else "" + imgui.pop_item_width() + + imgui.same_line() + imgui.text("Bias Profile:") + imgui.same_line() + bias_names = ["None"] + sorted(self.controller.bias_profiles.keys()) + b_idx = bias_names.index(self._editing_persona_bias_profile_id) if self._editing_persona_bias_profile_id in bias_names else 0 + imgui.push_item_width(200) + _, b_idx = imgui.combo("##pbiasprofile", b_idx, bias_names) + self._editing_persona_bias_profile_id = bias_names[b_idx] if b_idx > 0 else "" + imgui.pop_item_width() + + imgui.separator() + imgui.text("Preferred Models List:") + imgui.begin_child("pref_models_list", imgui.ImVec2(0, 200), True) + to_remove = [] + for i, entry in enumerate(self._editing_persona_preferred_models_list): + imgui.push_id(f"pref_model_{i}") + imgui.text(f"{i+1}.") + imgui.same_line() + + # Provider + imgui.set_next_item_width(120) + prov = entry.get("provider", "") + p_idx = providers.index(prov) + 1 if prov in providers else 0 + changed_p, p_idx = imgui.combo("##prov", p_idx, ["None"] + providers) + if changed_p: + entry["provider"] = providers[p_idx-1] if p_idx > 0 else "" + + imgui.same_line() + # Model + imgui.set_next_item_width(200) + curr_prov = entry.get("provider", "") + m_list = self.controller.all_available_models.get(curr_prov, []) + model = entry.get("model", "") + m_idx = m_list.index(model) + 1 if model in m_list else 0 + changed_m, m_idx = imgui.combo("##model", m_idx, ["None"] + m_list) + if changed_m: + entry["model"] = m_list[m_idx-1] if m_idx > 0 else "" + + imgui.same_line() + imgui.text("T:") + imgui.same_line() + # Temp + imgui.set_next_item_width(60) + _, entry["temperature"] = imgui.input_float("##temp", entry.get("temperature", 0.7), 0.1, 0.1, "%.1f") + + imgui.same_line() + imgui.text("M:") + imgui.same_line() + # MaxTok + imgui.set_next_item_width(80) + _, entry["max_output_tokens"] = imgui.input_int("##maxt", entry.get("max_output_tokens", 4096)) + + imgui.same_line() + if imgui.button("x"): + to_remove.append(i) + imgui.pop_id() + for i in reversed(to_remove): + self._editing_persona_preferred_models_list.pop(i) + imgui.end_child() + + if imgui.button("Add Preferred Model"): + self._editing_persona_preferred_models_list.append({ + "provider": self._editing_persona_provider or self.current_provider, + "model": self._editing_persona_model or self.current_model, + "temperature": self._editing_persona_temperature, + "max_output_tokens": self._editing_persona_max_tokens + }) + + imgui.separator() + imgui.text("System Prompt:") + + # Load Prompt Preset + imgui.text("Load from Preset:") + imgui.same_line() + prompt_presets = ["Select..."] + sorted(self.controller.presets.keys()) + if not hasattr(self, "_load_preset_idx"): self._load_preset_idx = 0 + imgui.push_item_width(150) + _, self._load_preset_idx = imgui.combo("##load_preset", self._load_preset_idx, prompt_presets) + imgui.pop_item_width() + imgui.same_line() + if imgui.button("Apply##apply_p"): + if self._load_preset_idx > 0: + pname = prompt_presets[self._load_preset_idx] + if pname in self.controller.presets: + p = self.controller.presets[pname] + self._editing_persona_system_prompt = p.system_prompt + if p.temperature is not None: self._editing_persona_temperature = p.temperature + if p.max_output_tokens is not None: self._editing_persona_max_tokens = p.max_output_tokens + self._load_preset_idx = 0 + + _, self._editing_persona_system_prompt = imgui.input_text_multiline("##pprompt", self._editing_persona_system_prompt, imgui.ImVec2(-1, 150)) + + imgui.separator() + if imgui.button("Save Persona", imgui.ImVec2(120, 0)): + if self._editing_persona_name.strip(): + try: + # If preferred list is empty, maybe we should add the default one? + # The models.Persona properties already handle returning None/default if list is empty. + # But for saving, we should probably ensure the list isn't empty if we have values. + save_models = copy.deepcopy(self._editing_persona_preferred_models_list) + if not save_models and self._editing_persona_model: + save_models.append({ + "provider": self._editing_persona_provider, + "model": self._editing_persona_model, + "temperature": self._editing_persona_temperature, + "max_output_tokens": self._editing_persona_max_tokens + }) + + persona = models.Persona( + name=self._editing_persona_name.strip(), + system_prompt=self._editing_persona_system_prompt, + tool_preset=self._editing_persona_tool_preset_id or None, + bias_profile=self._editing_persona_bias_profile_id or None, + preferred_models=save_models, + ) + self.controller._cb_save_persona(persona, self._editing_persona_scope) + self.ai_status = f"Saved Persona: {persona.name}" + except Exception as e: + self.ai_status = f"Error saving persona: {e}" + import traceback + traceback.print_exc() + else: + self.ai_status = "Name required" + + imgui.same_line() + if imgui.button("Delete", imgui.ImVec2(100, 0)): + if not self._editing_persona_is_new and self._editing_persona_name: + self.controller._cb_delete_persona(self._editing_persona_name, self._editing_persona_scope) + self.ai_status = f"Deleted Persona: {self._editing_persona_name}" + self._editing_persona_name = "" + self._editing_persona_is_new = True + + if not is_embedded: + imgui.same_line() + if imgui.button("Close", imgui.ImVec2(100, 0)): + self.show_persona_editor_window = False + finally: + imgui.end_child() + finally: + if not is_embedded: + imgui.end() def _render_projects_panel(self) -> None: @@ -2274,7 +2338,7 @@ def hello(): imgui.end_combo() imgui.same_line() if imgui.button("Manage Personas"): - self.show_persona_editor_modal = True + self.show_persona_editor_window = True if self.ui_active_persona and self.ui_active_persona in personas: persona = personas[self.ui_active_persona] self._editing_persona_name = persona.name @@ -3589,7 +3653,7 @@ def hello(): imgui.end_combo() imgui.same_line(0, 8) if imgui.button("Manage Presets##global"): - self.show_preset_manager_modal = True + self.show_preset_manager_window = True imgui.set_item_tooltip("Open preset management modal") ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100)) imgui.separator() @@ -3606,7 +3670,7 @@ def hello(): imgui.end_combo() imgui.same_line(0, 8) if imgui.button("Manage Presets##project"): - self.show_preset_manager_modal = True + self.show_preset_manager_window = True imgui.set_item_tooltip("Open preset management modal") ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100)) def _render_agent_tools_panel(self) -> None: @@ -3628,7 +3692,7 @@ def hello(): imgui.same_line() if imgui.button("Manage Presets##tools"): - self.show_tool_preset_manager_modal = True + self.show_tool_preset_manager_window = True if imgui.is_item_hovered(): imgui.set_tooltip("Configure tool availability and default modes.") @@ -3847,3 +3911,4 @@ def main() -> None: if __name__ == "__main__": main() + diff --git a/src/models.py b/src/models.py index 460ef0b..ec13288 100644 --- a/src/models.py +++ b/src/models.py @@ -434,32 +434,48 @@ class BiasProfile: @dataclass class Persona: name: str - provider: Optional[str] = None - model: Optional[str] = None - preferred_models: List[str] = field(default_factory=list) + preferred_models: List[Dict[str, Any]] = field(default_factory=list) system_prompt: str = '' - temperature: Optional[float] = None - top_p: Optional[float] = None - max_output_tokens: Optional[int] = None tool_preset: Optional[str] = None bias_profile: Optional[str] = None + @property + def provider(self) -> Optional[str]: + if not self.preferred_models: return None + return self.preferred_models[0].get("provider") + + @property + def model(self) -> Optional[str]: + if not self.preferred_models: return None + return self.preferred_models[0].get("model") + + @property + def temperature(self) -> Optional[float]: + if not self.preferred_models: return None + return self.preferred_models[0].get("temperature") + + @property + def top_p(self) -> Optional[float]: + if not self.preferred_models: return None + return self.preferred_models[0].get("top_p") + + @property + def max_output_tokens(self) -> Optional[int]: + if not self.preferred_models: return None + return self.preferred_models[0].get("max_output_tokens") + def to_dict(self) -> Dict[str, Any]: res = { "system_prompt": self.system_prompt, } - if self.provider is not None: - res["provider"] = self.provider - if self.model is not None: - res["model"] = self.model if self.preferred_models: - res["preferred_models"] = self.preferred_models - if self.temperature is not None: - res["temperature"] = self.temperature - if self.top_p is not None: - res["top_p"] = self.top_p - if self.max_output_tokens is not None: - res["max_output_tokens"] = self.max_output_tokens + processed = [] + for m in self.preferred_models: + if isinstance(m, str): + processed.append({"model": m}) + else: + processed.append(m) + res["preferred_models"] = processed if self.tool_preset is not None: res["tool_preset"] = self.tool_preset if self.bias_profile is not None: @@ -468,15 +484,34 @@ class Persona: @classmethod def from_dict(cls, name: str, data: Dict[str, Any]) -> "Persona": + raw_models = data.get("preferred_models", []) + parsed_models = [] + for m in raw_models: + if isinstance(m, str): + parsed_models.append({"model": m}) + else: + parsed_models.append(m) + + # Migration logic: merge legacy fields if they exist + legacy = {} + for k in ["provider", "model", "temperature", "top_p", "max_output_tokens"]: + if data.get(k) is not None: + legacy[k] = data[k] + + if legacy: + if not parsed_models: + parsed_models.append(legacy) + else: + # Merge into first item if it's missing these specific legacy fields + for k, v in legacy.items(): + if k not in parsed_models[0] or parsed_models[0][k] is None: + parsed_models[0][k] = v + return cls( name=name, - provider=data.get("provider"), - model=data.get("model"), - preferred_models=data.get("preferred_models", []), + preferred_models=parsed_models, system_prompt=data.get("system_prompt", ""), - temperature=data.get("temperature"), - top_p=data.get("top_p"), - max_output_tokens=data.get("max_output_tokens"), tool_preset=data.get("tool_preset"), bias_profile=data.get("bias_profile"), ) + diff --git a/tests/test_persona_manager.py b/tests/test_persona_manager.py index 29d9a8c..efc17cb 100644 --- a/tests/test_persona_manager.py +++ b/tests/test_persona_manager.py @@ -59,7 +59,7 @@ def test_load_all_merged(temp_paths): def test_save_persona(temp_paths): manager = PersonaManager(project_root=temp_paths["project_root"]) - persona = Persona(name="New", provider="gemini", system_prompt="Test") + persona = Persona(name="New", preferred_models=[{"provider": "gemini"}], system_prompt="Test") manager.save_persona(persona, scope="project") loaded = manager.load_all() diff --git a/tests/test_persona_models.py b/tests/test_persona_models.py index 04fdd24..bbd30fa 100644 --- a/tests/test_persona_models.py +++ b/tests/test_persona_models.py @@ -4,30 +4,38 @@ from src.models import Persona def test_persona_serialization(): persona = Persona( name="SecuritySpecialist", - provider="anthropic", - model="claude-3-7-sonnet-20250219", - preferred_models=["claude-3-7-sonnet-20250219", "claude-3-5-sonnet-20241022"], + preferred_models=[ + {"provider": "anthropic", "model": "claude-3-7-sonnet-20250219", "temperature": 0.2, "top_p": 0.9, "max_output_tokens": 4000}, + "claude-3-5-sonnet-20241022" + ], system_prompt="You are a security expert.", - temperature=0.2, - top_p=0.9, - max_output_tokens=4000, tool_preset="SecurityTools", bias_profile="Execution-Focused" ) + assert persona.provider == "anthropic" + assert persona.model == "claude-3-7-sonnet-20250219" + assert persona.temperature == 0.2 + assert persona.top_p == 0.9 + assert persona.max_output_tokens == 4000 + data = persona.to_dict() - assert data["provider"] == "anthropic" - assert data["model"] == "claude-3-7-sonnet-20250219" - assert "claude-3-5-sonnet-20241022" in data["preferred_models"] + # data should NOT have top-level provider/model anymore, it's in preferred_models + assert "provider" not in data + assert "model" not in data + assert data["preferred_models"][0]["provider"] == "anthropic" + assert data["preferred_models"][0]["model"] == "claude-3-7-sonnet-20250219" + assert data["preferred_models"][1] == {"model": "claude-3-5-sonnet-20241022"} assert data["system_prompt"] == "You are a security expert." - assert data["temperature"] == 0.2 - assert data["top_p"] == 0.9 - assert data["max_output_tokens"] == 4000 + assert data["preferred_models"][0]["temperature"] == 0.2 + assert data["preferred_models"][0]["top_p"] == 0.9 + assert data["preferred_models"][0]["max_output_tokens"] == 4000 assert data["tool_preset"] == "SecurityTools" assert data["bias_profile"] == "Execution-Focused" def test_persona_deserialization(): + # Old config format (legacy) data = { "provider": "gemini", "model": "gemini-2.5-flash", @@ -45,7 +53,9 @@ def test_persona_deserialization(): assert persona.name == "Assistant" assert persona.provider == "gemini" assert persona.model == "gemini-2.5-flash" - assert persona.preferred_models == ["gemini-2.5-flash"] + # Migration logic should have put legacy fields into preferred_models since it only had a string + assert persona.preferred_models[0]["provider"] == "gemini" + assert persona.preferred_models[0]["model"] == "gemini-2.5-flash" assert persona.system_prompt == "You are a helpful assistant." assert persona.temperature == 0.5 assert persona.top_p == 1.0