diff --git a/src/gui_2.py b/src/gui_2.py index af34abf..5e440a9 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -111,8 +111,8 @@ 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 = "[]" - self._editing_persona_tier_assignments = "{}" + self._editing_persona_preferred_models_list: list[str] = [] + self._editing_persona_scope = "project" self._editing_persona_is_new = True self._persona_editor_opened = False self._personas_list: dict[str, dict] = {} @@ -1233,112 +1233,201 @@ class App: 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.ImGuiCond_FirstUseEver) opened, self.show_persona_editor_modal = imgui.begin_popup_modal("Persona Editor", self.show_persona_editor_modal) if opened: try: - imgui.text("Name:") - imgui.same_line() - imgui.push_item_width(200) - _, self._editing_persona_name = imgui.input_text("##pname", self._editing_persona_name, 128) - imgui.pop_item_width() - 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(120) - _, 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:") - 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(150) - _, 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("MaxTok:") - imgui.same_line() - _, self._editing_persona_max_tokens = imgui.input_int("##pmaxt", self._editing_persona_max_tokens) + 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 = 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 + finally: + imgui.end_child() - # Tool Preset - imgui.text("Tool Preset:") imgui.same_line() - preset_names = ["None"] + sorted(self.controller.tool_presets.keys()) - t_idx = preset_names.index(self._editing_persona_tool_preset_id) if hasattr(self, '_editing_persona_tool_preset_id') and self._editing_persona_tool_preset_id in preset_names else 0 - imgui.push_item_width(150) - _, t_idx = imgui.combo("##ptoolpreset", t_idx, preset_names) - self._editing_persona_tool_preset_id = preset_names[t_idx] if t_idx > 0 else None - imgui.pop_item_width() - imgui.same_line() - if imgui.button("Manage Tools##pm"): - self.show_tool_preset_manager_modal = True - # Bias Profile - 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 hasattr(self, '_editing_persona_bias_profile_id') and self._editing_persona_bias_profile_id in bias_names else 0 - imgui.push_item_width(150) - _, b_idx = imgui.combo("##pbiasprofile", b_idx, bias_names) - self._editing_persona_bias_profile_id = bias_names[b_idx] if b_idx > 0 else None - imgui.pop_item_width() - - imgui.text("Pref Models (comma-separated):") - _, self._editing_persona_preferred_models = imgui.input_text("##pprefmodels", self._editing_persona_preferred_models, 256) + # 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 - imgui.text("Load Prompt Preset:") - imgui.same_line() - prompt_presets = ["None"] + sorted(self.controller.presets.keys()) - imgui.push_item_width(150) - changed_pr, pr_idx = imgui.combo("##load_prompt_preset", 0, prompt_presets) - imgui.pop_item_width() - if changed_pr and pr_idx > 0: - pname = prompt_presets[pr_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 - imgui.same_line() - if imgui.button("Manage Prompts##pm"): - self.show_preset_manager_modal = True - - imgui.text("Prompt:") - _, self._editing_persona_system_prompt = imgui.input_text_multiline("##pprompt", self._editing_persona_system_prompt, imgui.ImVec2(350, 50)) - if imgui.button("Save##p", imgui.ImVec2(80, 0)): - if self._editing_persona_name.strip(): - try: - pref_models = [m.strip() for m in self._editing_persona_preferred_models.split(",") if m.strip()] - if self._editing_persona_preferred_models.strip() == "[]": - pref_models = [] - 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=getattr(self, '_editing_persona_tool_preset_id', None), - bias_profile=getattr(self, '_editing_persona_bias_profile_id', None), - preferred_models=pref_models, - ) - self.controller._cb_save_persona(persona, "project") - self.ai_status = f"Saved: {persona.name}" - self.show_persona_editor_modal = False - imgui.close_current_popup() - except Exception as e: - self.ai_status = f"Error: {e}" - else: - self.ai_status = "Name required" - imgui.same_line() - if imgui.button("Cancel##p", imgui.ImVec2(80, 0)): - self.show_persona_editor_modal = False - imgui.close_current_popup() + _, 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: + 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" + + 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.same_line() + if imgui.button("Close", imgui.ImVec2(100, 0)): + self.show_persona_editor_modal = False + imgui.close_current_popup() + finally: + imgui.end_child() finally: imgui.end_popup() @@ -2184,19 +2273,20 @@ def hello(): ai_client.set_bias_profile(persona.bias_profile) imgui.end_combo() imgui.same_line() - if imgui.button("Edit##persona"): + if imgui.button("Manage Personas"): + self.show_persona_editor_modal = 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 self._editing_persona_provider = persona.provider or "" self._editing_persona_model = persona.model or "" self._editing_persona_system_prompt = persona.system_prompt or "" - self._editing_persona_temperature = persona.temperature or 0.7 - self._editing_persona_max_tokens = persona.max_output_tokens or 4096 + self._editing_persona_temperature = persona.temperature if persona.temperature is not None else 0.7 + self._editing_persona_max_tokens = persona.max_output_tokens if persona.max_output_tokens is not None else 4096 self._editing_persona_tool_preset_id = persona.tool_preset or "" self._editing_persona_bias_profile_id = persona.bias_profile or "" - import json - self._editing_persona_preferred_models = json.dumps(persona.preferred_models) if persona.preferred_models else "[]" + self._editing_persona_preferred_models_list = list(persona.preferred_models) if persona.preferred_models else [] + self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name) self._editing_persona_is_new = False else: self._editing_persona_name = "" @@ -2207,11 +2297,9 @@ def hello(): self._editing_persona_max_tokens = 4096 self._editing_persona_tool_preset_id = "" self._editing_persona_bias_profile_id = "" - self._editing_persona_preferred_models = "[]" - self._editing_persona_tier_assignments = "{}" + self._editing_persona_preferred_models_list = [] + self._editing_persona_scope = "project" self._editing_persona_is_new = True - self.show_persona_editor_modal = True - if self.current_provider == "gemini_cli": imgui.separator() imgui.text("Gemini CLI") diff --git a/src/personas.py b/src/personas.py index a3e6652..22792c5 100644 --- a/src/personas.py +++ b/src/personas.py @@ -47,6 +47,21 @@ class PersonaManager: data["personas"][persona.name] = persona.to_dict() self._save_file(path, data) + def get_persona_scope(self, name: str) -> str: + """Returns the scope ('global' or 'project') of a persona by name.""" + if self.project_root: + project_path = paths.get_project_personas_path(self.project_root) + project_data = self._load_file(project_path) + if name in project_data.get("personas", {}): + return "project" + + global_path = paths.get_global_personas_path() + global_data = self._load_file(global_path) + if name in global_data.get("personas", {}): + return "global" + + return "project" + def delete_persona(self, name: str, scope: str = "project") -> None: path = self._get_path(scope) data = self._load_file(path)