diff --git a/gui_2.py b/gui_2.py index 222120d..70c0b09 100644 --- a/gui_2.py +++ b/gui_2.py @@ -158,17 +158,10 @@ class App: self.models_thread: threading.Thread | None = None _default_windows = { - "Projects": True, - "Files": True, - "Screenshots": True, - "Discussion History": True, - "Provider": True, - "Message": True, - "Response": True, - "Tool Calls": True, - "Comms History": True, - "System Prompts": True, - "Theme": True, + "Context Hub": True, + "AI Settings Hub": True, + "Discussion Hub": True, + "Operations Hub": True, "Diagnostics": False, } saved = self.config.get("gui", {}).get("show_windows", {}) @@ -689,786 +682,71 @@ class App: # imgui.end_menu() # imgui.end_main_menu_bar() - # ---- Projects - if self.show_windows["Projects"]: - exp, self.show_windows["Projects"] = imgui.begin("Projects", self.show_windows["Projects"]) + + # ---- Context Hub + if self.show_windows.get("Context Hub", False): + exp, self.show_windows["Context Hub"] = imgui.begin("Context Hub", self.show_windows["Context Hub"]) if exp: - proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem) - imgui.text_colored(C_IN, f"Active: {proj_name}") - imgui.separator() - imgui.text("Git Directory") - ch, self.ui_project_git_dir = imgui.input_text("##git_dir", self.ui_project_git_dir) - imgui.same_line() - if imgui.button("Browse##git"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Git Directory") - r.destroy() - if d: self.ui_project_git_dir = d - - imgui.separator() - imgui.text("Main Context File") - ch, self.ui_project_main_context = imgui.input_text("##main_ctx", self.ui_project_main_context) - imgui.same_line() - if imgui.button("Browse##ctx"): - r = hide_tk_root() - p = filedialog.askopenfilename(title="Select Main Context File") - r.destroy() - if p: self.ui_project_main_context = p - - imgui.separator() - imgui.text("Output Dir") - ch, self.ui_output_dir = imgui.input_text("##out_dir", self.ui_output_dir) - imgui.same_line() - if imgui.button("Browse##out"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Output Dir") - r.destroy() - if d: self.ui_output_dir = d - - imgui.separator() - imgui.text("Project Files") - imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) - for i, pp in enumerate(self.project_paths): - is_active = (pp == self.active_project_path) - if imgui.button(f"x##p{i}"): - removed = self.project_paths.pop(i) - if removed == self.active_project_path and self.project_paths: - self._switch_project(self.project_paths[0]) - break - imgui.same_line() - marker = " *" if is_active else "" - if is_active: imgui.push_style_color(imgui.Col_.text, C_IN) - if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): - self._switch_project(pp) - if is_active: imgui.pop_style_color() - imgui.same_line() - imgui.text_colored(C_LBL, pp) - imgui.end_child() - - if imgui.button("Add Project"): - r = hide_tk_root() - p = filedialog.askopenfilename( - title="Select Project .toml", - filetypes=[("TOML", "*.toml"), ("All", "*.*")], - ) - r.destroy() - if p and p not in self.project_paths: - self.project_paths.append(p) - imgui.same_line() - if imgui.button("New Project"): - r = hide_tk_root() - p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")]) - r.destroy() - if p: - name = Path(p).stem - proj = project_manager.default_project(name) - project_manager.save_project(proj, p) - if p not in self.project_paths: - self.project_paths.append(p) - self._switch_project(p) - imgui.same_line() - if imgui.button("Save All"): - self._flush_to_project() - self._save_active_project() - self._flush_to_config() - save_config(self.config) - self.ai_status = "config saved" - - ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap) - ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only) - - if imgui.collapsing_header("Agent Tools"): - for t_name in AGENT_TOOL_NAMES: - val = self.ui_agent_tools.get(t_name, True) - ch, val = imgui.checkbox(f"Enable {t_name}", val) - if ch: - self.ui_agent_tools[t_name] = val + if imgui.begin_tab_bar("ContextTabs"): + if imgui.begin_tab_item("Projects")[0]: + self._render_projects_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Files")[0]: + self._render_files_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Screenshots")[0]: + self._render_screenshots_panel() + imgui.end_tab_item() + imgui.end_tab_bar() imgui.end() - # ---- Files - if self.show_windows["Files"]: - exp, self.show_windows["Files"] = imgui.begin("Files", self.show_windows["Files"]) + # ---- AI Settings Hub + if self.show_windows.get("AI Settings Hub", False): + exp, self.show_windows["AI Settings Hub"] = imgui.begin("AI Settings Hub", self.show_windows["AI Settings Hub"]) if exp: - imgui.text("Base Dir") - ch, self.ui_files_base_dir = imgui.input_text("##f_base", self.ui_files_base_dir) - imgui.same_line() - if imgui.button("Browse##fb"): - r = hide_tk_root() - d = filedialog.askdirectory() - r.destroy() - if d: self.ui_files_base_dir = d - - imgui.separator() - imgui.text("Paths") - imgui.begin_child("f_paths", imgui.ImVec2(0, -40), True) - for i, f in enumerate(self.files): - if imgui.button(f"x##f{i}"): - self.files.pop(i) - break - imgui.same_line() - imgui.text(f) - imgui.end_child() - - if imgui.button("Add File(s)"): - r = hide_tk_root() - paths = filedialog.askopenfilenames() - r.destroy() - for p in paths: - if p not in self.files: self.files.append(p) - imgui.same_line() - if imgui.button("Add Wildcard"): - r = hide_tk_root() - d = filedialog.askdirectory() - r.destroy() - if d: self.files.append(str(Path(d) / "**" / "*")) + if imgui.begin_tab_bar("AISettingsTabs"): + if imgui.begin_tab_item("Provider")[0]: + self._render_provider_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("System Prompts")[0]: + self._render_system_prompts_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Theme")[0]: + self._render_theme_panel() + imgui.end_tab_item() + imgui.end_tab_bar() imgui.end() - # ---- Screenshots - if self.show_windows["Screenshots"]: - exp, self.show_windows["Screenshots"] = imgui.begin("Screenshots", self.show_windows["Screenshots"]) + # ---- Discussion Hub + if self.show_windows.get("Discussion Hub", False): + exp, self.show_windows["Discussion Hub"] = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"]) if exp: - imgui.text("Base Dir") - ch, self.ui_shots_base_dir = imgui.input_text("##s_base", self.ui_shots_base_dir) - imgui.same_line() - if imgui.button("Browse##sb"): - r = hide_tk_root() - d = filedialog.askdirectory() - r.destroy() - if d: self.ui_shots_base_dir = d - - imgui.separator() - imgui.text("Paths") - imgui.begin_child("s_paths", imgui.ImVec2(0, -40), True) - for i, s in enumerate(self.screenshots): - if imgui.button(f"x##s{i}"): - self.screenshots.pop(i) - break - imgui.same_line() - imgui.text(s) - imgui.end_child() - - if imgui.button("Add Screenshot(s)"): - r = hide_tk_root() - paths = filedialog.askopenfilenames( - title="Select Screenshots", - filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")], - ) - r.destroy() - for p in paths: - if p not in self.screenshots: self.screenshots.append(p) + if imgui.begin_tab_bar("DiscussionTabs"): + if imgui.begin_tab_item("History")[0]: + self._render_discussion_panel() + imgui.end_tab_item() + imgui.end_tab_bar() imgui.end() - # ---- Discussion History - if self.show_windows["Discussion History"]: - exp, self.show_windows["Discussion History"] = imgui.begin("Discussion History", self.show_windows["Discussion History"]) + # ---- Operations Hub + if self.show_windows.get("Operations Hub", False): + exp, self.show_windows["Operations Hub"] = imgui.begin("Operations Hub", self.show_windows["Operations Hub"]) if exp: - # THINKING indicator - is_thinking = self.ai_status in ["sending..."] - if is_thinking: - val = math.sin(time.time() * 10 * math.pi) - alpha = 1.0 if val > 0 else 0.0 - imgui.text_colored(imgui.ImVec4(1.0, 0.39, 0.39, alpha), "THINKING...") - imgui.separator() - - # Prior session viewing mode - if self.is_viewing_prior_session: - imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20)) - imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION") - imgui.same_line() - if imgui.button("Exit Prior Session"): - self.is_viewing_prior_session = False - self.prior_session_entries.clear() - imgui.separator() - imgui.begin_child("prior_scroll", imgui.ImVec2(0, 0), False) - for idx, entry in enumerate(self.prior_session_entries): - imgui.push_id(f"prior_{idx}") - kind = entry.get("kind", entry.get("type", "")) - imgui.text_colored(C_LBL, f"#{idx+1}") - imgui.same_line() - ts = entry.get("ts", entry.get("timestamp", "")) - if ts: - imgui.text_colored(vec4(160, 160, 160), str(ts)) - imgui.same_line() - imgui.text_colored(C_KEY, str(kind)) - payload = entry.get("payload", entry) - text = payload.get("text", payload.get("message", payload.get("content", ""))) - if text: - preview = str(text).replace("\n", " ")[:200] - if self.ui_word_wrap: - imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(preview) - imgui.pop_text_wrap_pos() - else: - imgui.text(preview) - imgui.separator() - imgui.pop_id() - imgui.end_child() - imgui.pop_style_color() - - if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): - names = self._get_discussion_names() - - if imgui.begin_combo("##disc_sel", self.active_discussion): - for name in names: - is_selected = (name == self.active_discussion) - if imgui.selectable(name, is_selected)[0]: - self._switch_discussion(name) - if is_selected: - imgui.set_item_default_focus() - imgui.end_combo() - - disc_sec = self.project.get("discussion", {}) - disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {}) - git_commit = disc_data.get("git_commit", "") - last_updated = disc_data.get("last_updated", "") - - imgui.text_colored(C_LBL, "commit:") - imgui.same_line() - imgui.text_colored(C_IN if git_commit else C_LBL, git_commit[:12] if git_commit else "(none)") - imgui.same_line() - if imgui.button("Update Commit"): - git_dir = self.ui_project_git_dir - if git_dir: - cmt = project_manager.get_git_commit(git_dir) - if cmt: - disc_data["git_commit"] = cmt - disc_data["last_updated"] = project_manager.now_ts() - self.ai_status = f"commit: {cmt[:12]}" - - imgui.text_colored(C_LBL, "updated:") - imgui.same_line() - imgui.text_colored(C_SUB, last_updated if last_updated else "(never)") - - ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input) - imgui.same_line() - if imgui.button("Create"): - nm = self.ui_disc_new_name_input.strip() - if nm: self._create_discussion(nm); self.ui_disc_new_name_input = "" - imgui.same_line() - if imgui.button("Rename"): - nm = self.ui_disc_new_name_input.strip() - if nm: self._rename_discussion(self.active_discussion, nm); self.ui_disc_new_name_input = "" - imgui.same_line() - if imgui.button("Delete"): - self._delete_discussion(self.active_discussion) - - if not self.is_viewing_prior_session: - imgui.separator() - if imgui.button("+ Entry"): - self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("-All"): - for e in self.disc_entries: e["collapsed"] = True - imgui.same_line() - if imgui.button("+All"): - for e in self.disc_entries: e["collapsed"] = False - imgui.same_line() - if imgui.button("Clear All"): - self.disc_entries.clear() - imgui.same_line() - if imgui.button("Save"): - self._flush_to_project() - self._save_active_project() - self._flush_to_config() - save_config(self.config) - self.ai_status = "discussion saved" - imgui.same_line() - if imgui.button("Load Log"): - self.cb_load_prior_log() - - ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history) - - # Truncation controls - imgui.text("Keep Pairs:") - imgui.same_line() - imgui.set_next_item_width(80) - ch, self.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", self.ui_disc_truncate_pairs, 1) - if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1 - imgui.same_line() - if imgui.button("Truncate"): - self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs) - self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs" - imgui.separator() - - if imgui.collapsing_header("Roles"): - imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True) - for i, r in enumerate(self.disc_roles): - if imgui.button(f"x##r{i}"): - self.disc_roles.pop(i) - break - imgui.same_line() - imgui.text(r) - imgui.end_child() - ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input) - imgui.same_line() - if imgui.button("Add"): - r = self.ui_disc_new_role_input.strip() - if r and r not in self.disc_roles: - self.disc_roles.append(r) - self.ui_disc_new_role_input = "" - - imgui.separator() - imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False) - for i, entry in enumerate(self.disc_entries): - imgui.push_id(str(i)) - collapsed = entry.get("collapsed", False) - read_mode = entry.get("read_mode", False) - - if imgui.button("+" if collapsed else "-"): - entry["collapsed"] = not collapsed - imgui.same_line() - - imgui.set_next_item_width(120) - if imgui.begin_combo("##role", entry["role"]): - for r in self.disc_roles: - if imgui.selectable(r, r == entry["role"])[0]: - entry["role"] = r - imgui.end_combo() - - if not collapsed: - imgui.same_line() - if imgui.button("[Edit]" if read_mode else "[Read]"): - entry["read_mode"] = not read_mode - - ts_str = entry.get("ts", "") - if ts_str: - imgui.same_line() - imgui.text_colored(vec4(120, 120, 100), ts_str) - - if collapsed: - imgui.same_line() - if imgui.button("Ins"): - self.disc_entries.insert(i, {"role": "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()}) - imgui.same_line() - self._render_text_viewer(f"Entry #{i+1}", entry["content"]) - imgui.same_line() - if imgui.button("Del"): - self.disc_entries.pop(i) - imgui.pop_id() - break - imgui.same_line() - preview = entry["content"].replace("\n", " ")[:60] - if len(entry["content"]) > 60: preview += "..." - imgui.text_colored(vec4(160, 160, 150), preview) - - if not collapsed: - if read_mode: - imgui.begin_child("read_content", imgui.ImVec2(0, 150), True) - if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(entry["content"]) - if self.ui_word_wrap: imgui.pop_text_wrap_pos() - imgui.end_child() - else: - ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) - - imgui.separator() - imgui.pop_id() - if self._scroll_disc_to_bottom: - imgui.set_scroll_here_y(1.0) - self._scroll_disc_to_bottom = False - imgui.end_child() + if imgui.begin_tab_bar("OperationsTabs"): + if imgui.begin_tab_item("Message")[0]: + self._render_message_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Response")[0]: + self._render_response_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Tool Calls")[0]: + self._render_tool_calls_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Comms History")[0]: + self._render_comms_history_panel() + imgui.end_tab_item() + imgui.end_tab_bar() imgui.end() - - # ---- Provider - if self.show_windows["Provider"]: - exp, self.show_windows["Provider"] = imgui.begin("Provider", self.show_windows["Provider"]) - if exp: - imgui.text("Provider") - if imgui.begin_combo("##prov", self.current_provider): - for p in PROVIDERS: - if imgui.selectable(p, p == self.current_provider)[0]: - self.current_provider = p - ai_client.reset_session() - ai_client.set_provider(p, self.current_model) - self.available_models = [] - self._fetch_models(p) - imgui.end_combo() - imgui.separator() - imgui.text("Model") - imgui.same_line() - if imgui.button("Fetch Models"): - self._fetch_models(self.current_provider) - - if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): - for m in self.available_models: - if imgui.selectable(m, m == self.current_model)[0]: - self.current_model = m - ai_client.reset_session() - ai_client.set_provider(self.current_provider, m) - imgui.end_list_box() - imgui.separator() - imgui.text("Parameters") - ch, self.temperature = imgui.slider_float("Temperature", self.temperature, 0.0, 2.0, "%.2f") - ch, self.max_tokens = imgui.input_int("Max Tokens (Output)", self.max_tokens, 1024) - ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024) - - imgui.separator() - imgui.text("Telemetry") - usage = self.session_usage - total = usage["input_tokens"] + usage["output_tokens"] - imgui.text_colored(C_RES, f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})") - if usage["cache_read_input_tokens"]: - imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}") - imgui.text("Token Budget:") - imgui.progress_bar(self._token_budget_pct, imgui.ImVec2(-1, 0), f"{self._token_budget_current:,} / {self._token_budget_limit:,}") - if self._gemini_cache_text: - imgui.text_colored(C_SUB, self._gemini_cache_text) - imgui.end() - - # ---- Message - if self.show_windows["Message"]: - exp, self.show_windows["Message"] = imgui.begin("Message", self.show_windows["Message"]) - if exp: - # LIVE indicator - is_live = self.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] - if is_live: - val = math.sin(time.time() * 10 * math.pi) - alpha = 1.0 if val > 0 else 0.0 - imgui.text_colored(imgui.ImVec4(0.39, 1.0, 0.39, alpha), "LIVE") - imgui.separator() - - ch, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.ui_ai_input, imgui.ImVec2(-1, -40)) - - # Keyboard shortcuts - io = imgui.get_io() - ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) - ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) - if ctrl_l: - self.ui_ai_input = "" - - imgui.separator() - send_busy = False - with self._send_thread_lock: - if self.send_thread and self.send_thread.is_alive(): - send_busy = True - if imgui.button("Gen + Send") or ctrl_enter: - if not send_busy: - try: - md, path, file_items, stable_md, disc_text = self._do_generate() - self.last_md = md - self.last_md_path = path - self.last_file_items = file_items - except Exception as e: - self.ai_status = f"generate error: {e}" - else: - self.ai_status = "sending..." - user_msg = self.ui_ai_input - base_dir = self.ui_files_base_dir - csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()]) - ai_client.set_custom_system_prompt("\n\n".join(csp)) - ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit) - ai_client.set_agent_tools(self.ui_agent_tools) - send_md = stable_md - send_disc = disc_text - - def do_send(): - if self.ui_auto_add_history: - with self._pending_history_adds_lock: - self._pending_history_adds.append({"role": "User", "content": user_msg, "collapsed": False, "ts": project_manager.now_ts()}) - try: - resp = ai_client.send(send_md, user_msg, base_dir, self.last_file_items, send_disc) - self.ai_response = resp - self.ai_status = "done" - self._trigger_blink = True - if self.ui_auto_add_history: - with self._pending_history_adds_lock: - self._pending_history_adds.append({"role": "AI", "content": resp, "collapsed": False, "ts": project_manager.now_ts()}) - except ProviderError as e: - self.ai_response = e.ui_message() - self.ai_status = "error" - self._trigger_blink = True - if self.ui_auto_add_history: - with self._pending_history_adds_lock: - self._pending_history_adds.append({"role": "Vendor API", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()}) - except Exception as e: - self.ai_response = f"ERROR: {e}" - self.ai_status = "error" - self._trigger_blink = True - if self.ui_auto_add_history: - with self._pending_history_adds_lock: - self._pending_history_adds.append({"role": "System", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()}) - - with self._send_thread_lock: - self.send_thread = threading.Thread(target=do_send, daemon=True) - self.send_thread.start() - imgui.same_line() - if imgui.button("MD Only"): - try: - md, path, *_ = self._do_generate() - self.last_md = md - self.last_md_path = path - self.ai_status = f"md written: {path.name}" - except Exception as e: - self.ai_status = f"error: {e}" - imgui.same_line() - if imgui.button("Reset"): - ai_client.reset_session() - ai_client.clear_comms_log() - self._tool_log.clear() - self._comms_log.clear() - self.ai_status = "session reset" - self.ai_response = "" - imgui.same_line() - if imgui.button("-> History"): - if self.ui_ai_input: - self.disc_entries.append({"role": "User", "content": self.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) - imgui.end() - - # ---- Response - if self.show_windows["Response"]: - - if self._trigger_blink: - self._trigger_blink = False - self._is_blinking = True - self._blink_start_time = time.time() - imgui.set_window_focus_str("Response") - - if self._is_blinking: - elapsed = time.time() - self._blink_start_time - if elapsed > 1.5: - self._is_blinking = False - else: - val = math.sin(elapsed * 8 * math.pi) - alpha = 50/255 if val > 0 else 0 - imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 255, 0, alpha)) - imgui.push_style_color(imgui.Col_.child_bg, vec4(0, 255, 0, alpha)) - - exp, self.show_windows["Response"] = imgui.begin("Response", self.show_windows["Response"]) - if exp: - if self.ui_word_wrap: - imgui.begin_child("resp_wrap", imgui.ImVec2(-1, -40), True) - imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(self.ai_response) - imgui.pop_text_wrap_pos() - imgui.end_child() - else: - imgui.input_text_multiline("##ai_out", self.ai_response, imgui.ImVec2(-1, -40), imgui.InputTextFlags_.read_only) - imgui.separator() - if imgui.button("-> History"): - if self.ai_response: - self.disc_entries.append({"role": "AI", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()}) - - if self._is_blinking: - imgui.pop_style_color(2) - imgui.end() - - # ---- Tool Calls - if self.show_windows["Tool Calls"]: - exp, self.show_windows["Tool Calls"] = imgui.begin("Tool Calls", self.show_windows["Tool Calls"]) - if exp: - imgui.text("Tool call history") - imgui.same_line() - if imgui.button("Clear##tc"): - self._tool_log.clear() - imgui.separator() - imgui.begin_child("tc_scroll") - for i, (script, result) in enumerate(self._tool_log, 1): - first_line = script.strip().splitlines()[0][:80] if script.strip() else "(empty)" - imgui.text_colored(C_KEY, f"Call #{i}: {first_line}") - imgui.same_line() - self._render_text_viewer(f"Call Script #{i}", script) - imgui.same_line() - self._render_text_viewer(f"Call Output #{i}", result) - - if self.ui_word_wrap: - imgui.begin_child(f"tc_wrap_{i}", imgui.ImVec2(-1, 72), True) - imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(result) - imgui.pop_text_wrap_pos() - imgui.end_child() - else: - imgui.input_text_multiline(f"##tc_res_{i}", result, imgui.ImVec2(-1, 72), imgui.InputTextFlags_.read_only) - imgui.separator() - imgui.end_child() - imgui.end() - - # ---- Comms History - if self.show_windows["Comms History"]: - exp, self.show_windows["Comms History"] = imgui.begin("Comms History", self.show_windows["Comms History"]) - if exp: - imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}") - imgui.same_line() - if imgui.button("Clear##comms"): - ai_client.clear_comms_log() - self._comms_log.clear() - imgui.separator() - - imgui.text_colored(C_OUT, "OUT") - imgui.same_line() - imgui.text_colored(C_REQ, "request") - imgui.same_line() - imgui.text_colored(C_TC, "tool_call") - imgui.same_line() - imgui.text(" ") - imgui.same_line() - imgui.text_colored(C_IN, "IN") - imgui.same_line() - imgui.text_colored(C_RES, "response") - imgui.same_line() - imgui.text_colored(C_TR, "tool_result") - imgui.separator() - - imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) - for idx, entry in enumerate(self._comms_log, 1): - imgui.push_id(f"comms_{idx}") - d = entry["direction"] - k = entry["kind"] - - imgui.text_colored(vec4(160, 160, 160), f"#{idx}") - imgui.same_line() - imgui.text_colored(vec4(160, 160, 160), entry["ts"]) - imgui.same_line() - imgui.text_colored(DIR_COLORS.get(d, C_VAL), d) - imgui.same_line() - imgui.text_colored(KIND_COLORS.get(k, C_VAL), k) - imgui.same_line() - imgui.text_colored(C_LBL, f"{entry['provider']}/{entry['model']}") - - payload = entry["payload"] - - if k == "request": - self._render_heavy_text("message", payload.get("message", "")) - elif k == "response": - imgui.text_colored(C_LBL, "round:") - imgui.same_line() - imgui.text_colored(C_VAL, str(payload.get("round", ""))) - - imgui.text_colored(C_LBL, "stop_reason:") - imgui.same_line() - imgui.text_colored(vec4(255, 200, 120), str(payload.get("stop_reason", ""))) - - text = payload.get("text", "") - if text: - self._render_heavy_text("text", text) - - imgui.text_colored(C_LBL, "tool_calls:") - tcs = payload.get("tool_calls", []) - if not tcs: - imgui.text_colored(C_VAL, " (none)") - for i, tc in enumerate(tcs): - imgui.text_colored(C_KEY, f" call[{i}] {tc.get('name', '?')}") - if "id" in tc: - imgui.text_colored(C_LBL, " id:") - imgui.same_line() - imgui.text_colored(C_VAL, str(tc["id"])) - args = tc.get("args") or tc.get("input") or {} - if isinstance(args, dict): - for ak, av in args.items(): - self._render_heavy_text(f" {ak}", str(av)) - elif args: - self._render_heavy_text(" args", str(args)) - - usage = payload.get("usage") - if usage: - imgui.text_colored(C_SUB, "usage:") - for uk, uv in usage.items(): - imgui.text_colored(C_LBL, f" {uk.replace('_', ' ')}:") - imgui.same_line() - imgui.text_colored(C_NUM, str(uv)) - - elif k == "tool_call": - imgui.text_colored(C_LBL, "name:") - imgui.same_line() - imgui.text_colored(C_VAL, str(payload.get("name", ""))) - if "id" in payload: - imgui.text_colored(C_LBL, "id:") - imgui.same_line() - imgui.text_colored(C_VAL, str(payload["id"])) - if "script" in payload: - self._render_heavy_text("script", payload.get("script", "")) - elif "args" in payload: - args = payload["args"] - if isinstance(args, dict): - for ak, av in args.items(): - self._render_heavy_text(ak, str(av)) - else: - self._render_heavy_text("args", str(args)) - - elif k == "tool_result": - imgui.text_colored(C_LBL, "name:") - imgui.same_line() - imgui.text_colored(C_VAL, str(payload.get("name", ""))) - if "id" in payload: - imgui.text_colored(C_LBL, "id:") - imgui.same_line() - imgui.text_colored(C_VAL, str(payload["id"])) - self._render_heavy_text("output", payload.get("output", "")) - - elif k == "tool_result_send": - for i, r in enumerate(payload.get("results", [])): - imgui.text_colored(C_KEY, f"result[{i}]") - imgui.text_colored(C_LBL, " tool_use_id:") - imgui.same_line() - imgui.text_colored(C_VAL, str(r.get("tool_use_id", ""))) - self._render_heavy_text(" content", str(r.get("content", ""))) - else: - for key, val in payload.items(): - vstr = json.dumps(val, ensure_ascii=False, indent=2) if isinstance(val, (dict, list)) else str(val) - if key in HEAVY_KEYS: - self._render_heavy_text(key, vstr) - else: - imgui.text_colored(C_LBL, f"{key}:") - imgui.same_line() - imgui.text_colored(C_VAL, vstr) - - imgui.separator() - imgui.pop_id() - imgui.end_child() - imgui.end() - - # ---- System Prompts - if self.show_windows["System Prompts"]: - exp, self.show_windows["System Prompts"] = imgui.begin("System Prompts", self.show_windows["System Prompts"]) - if exp: - imgui.text("Global System Prompt (all projects)") - ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100)) - imgui.separator() - imgui.text("Project System Prompt") - ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100)) - imgui.end() - - # ---- Theme - if self.show_windows["Theme"]: - exp, self.show_windows["Theme"] = imgui.begin("Theme", self.show_windows["Theme"]) - if exp: - imgui.text("Palette") - cp = theme.get_current_palette() - if imgui.begin_combo("##pal", cp): - for p in theme.get_palette_names(): - if imgui.selectable(p, p == cp)[0]: - theme.apply(p) - imgui.end_combo() - imgui.separator() - imgui.text("Font") - imgui.push_item_width(-150) - ch, path = imgui.input_text("##fontp", theme.get_current_font_path()) - imgui.pop_item_width() - if ch: theme._current_font_path = path - imgui.same_line() - if imgui.button("Browse##font"): - r = hide_tk_root() - p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")]) - r.destroy() - if p: theme._current_font_path = p - - imgui.text("Size (px)") - imgui.same_line() - imgui.push_item_width(100) - ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f") - if ch: theme._current_font_size = size - imgui.pop_item_width() - imgui.same_line() - if imgui.button("Apply Font (Requires Restart)"): - self._flush_to_config() - save_config(self.config) - self.ai_status = "Font settings saved. Restart required." - - imgui.separator() - imgui.text("UI Scale (DPI)") - ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") - if ch: theme.set_scale(scale) - imgui.end() - # ---- Diagnostics if self.show_windows["Diagnostics"]: exp, self.show_windows["Diagnostics"] = imgui.begin("Diagnostics", self.show_windows["Diagnostics"]) @@ -1632,6 +910,742 @@ class App: imgui.input_text_multiline("##tv_c", self.text_viewer_content, imgui.ImVec2(-1, -1), imgui.InputTextFlags_.read_only) imgui.end() + def _render_projects_panel(self): + proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem) + imgui.text_colored(C_IN, f"Active: {proj_name}") + imgui.separator() + imgui.text("Git Directory") + ch, self.ui_project_git_dir = imgui.input_text("##git_dir", self.ui_project_git_dir) + imgui.same_line() + if imgui.button("Browse##git"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Git Directory") + r.destroy() + if d: self.ui_project_git_dir = d + + imgui.separator() + imgui.text("Main Context File") + ch, self.ui_project_main_context = imgui.input_text("##main_ctx", self.ui_project_main_context) + imgui.same_line() + if imgui.button("Browse##ctx"): + r = hide_tk_root() + p = filedialog.askopenfilename(title="Select Main Context File") + r.destroy() + if p: self.ui_project_main_context = p + + imgui.separator() + imgui.text("Output Dir") + ch, self.ui_output_dir = imgui.input_text("##out_dir", self.ui_output_dir) + imgui.same_line() + if imgui.button("Browse##out"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Output Dir") + r.destroy() + if d: self.ui_output_dir = d + + imgui.separator() + imgui.text("Project Files") + imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) + for i, pp in enumerate(self.project_paths): + is_active = (pp == self.active_project_path) + if imgui.button(f"x##p{i}"): + removed = self.project_paths.pop(i) + if removed == self.active_project_path and self.project_paths: + self._switch_project(self.project_paths[0]) + break + imgui.same_line() + marker = " *" if is_active else "" + if is_active: imgui.push_style_color(imgui.Col_.text, C_IN) + if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): + self._switch_project(pp) + if is_active: imgui.pop_style_color() + imgui.same_line() + imgui.text_colored(C_LBL, pp) + imgui.end_child() + + if imgui.button("Add Project"): + r = hide_tk_root() + p = filedialog.askopenfilename( + title="Select Project .toml", + filetypes=[("TOML", "*.toml"), ("All", "*.*")], + ) + r.destroy() + if p and p not in self.project_paths: + self.project_paths.append(p) + imgui.same_line() + if imgui.button("New Project"): + r = hide_tk_root() + p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")]) + r.destroy() + if p: + name = Path(p).stem + proj = project_manager.default_project(name) + project_manager.save_project(proj, p) + if p not in self.project_paths: + self.project_paths.append(p) + self._switch_project(p) + imgui.same_line() + if imgui.button("Save All"): + self._flush_to_project() + self._save_active_project() + self._flush_to_config() + save_config(self.config) + self.ai_status = "config saved" + + ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap) + ch, self.ui_summary_only = imgui.checkbox("Summary Only (send file structure, not full content)", self.ui_summary_only) + + if imgui.collapsing_header("Agent Tools"): + for t_name in AGENT_TOOL_NAMES: + val = self.ui_agent_tools.get(t_name, True) + ch, val = imgui.checkbox(f"Enable {t_name}", val) + if ch: + self.ui_agent_tools[t_name] = val + + def _render_files_panel(self): + imgui.text("Base Dir") + ch, self.ui_files_base_dir = imgui.input_text("##f_base", self.ui_files_base_dir) + imgui.same_line() + if imgui.button("Browse##fb"): + r = hide_tk_root() + d = filedialog.askdirectory() + r.destroy() + if d: self.ui_files_base_dir = d + + imgui.separator() + imgui.text("Paths") + imgui.begin_child("f_paths", imgui.ImVec2(0, -40), True) + for i, f in enumerate(self.files): + if imgui.button(f"x##f{i}"): + self.files.pop(i) + break + imgui.same_line() + imgui.text(f) + imgui.end_child() + + if imgui.button("Add File(s)"): + r = hide_tk_root() + paths = filedialog.askopenfilenames() + r.destroy() + for p in paths: + if p not in self.files: self.files.append(p) + imgui.same_line() + if imgui.button("Add Wildcard"): + r = hide_tk_root() + d = filedialog.askdirectory() + r.destroy() + if d: self.files.append(str(Path(d) / "**" / "*")) + + def _render_screenshots_panel(self): + imgui.text("Base Dir") + ch, self.ui_shots_base_dir = imgui.input_text("##s_base", self.ui_shots_base_dir) + imgui.same_line() + if imgui.button("Browse##sb"): + r = hide_tk_root() + d = filedialog.askdirectory() + r.destroy() + if d: self.ui_shots_base_dir = d + + imgui.separator() + imgui.text("Paths") + imgui.begin_child("s_paths", imgui.ImVec2(0, -40), True) + for i, s in enumerate(self.screenshots): + if imgui.button(f"x##s{i}"): + self.screenshots.pop(i) + break + imgui.same_line() + imgui.text(s) + imgui.end_child() + + if imgui.button("Add Screenshot(s)"): + r = hide_tk_root() + paths = filedialog.askopenfilenames( + title="Select Screenshots", + filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")], + ) + r.destroy() + for p in paths: + if p not in self.screenshots: self.screenshots.append(p) + + def _render_discussion_panel(self): + # THINKING indicator + is_thinking = self.ai_status in ["sending..."] + if is_thinking: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + imgui.text_colored(imgui.ImVec4(1.0, 0.39, 0.39, alpha), "THINKING...") + imgui.separator() + + # Prior session viewing mode + if self.is_viewing_prior_session: + imgui.push_style_color(imgui.Col_.child_bg, vec4(50, 40, 20)) + imgui.text_colored(vec4(255, 200, 100), "VIEWING PRIOR SESSION") + imgui.same_line() + if imgui.button("Exit Prior Session"): + self.is_viewing_prior_session = False + self.prior_session_entries.clear() + imgui.separator() + imgui.begin_child("prior_scroll", imgui.ImVec2(0, 0), False) + for idx, entry in enumerate(self.prior_session_entries): + imgui.push_id(f"prior_{idx}") + kind = entry.get("kind", entry.get("type", "")) + imgui.text_colored(C_LBL, f"#{idx+1}") + imgui.same_line() + ts = entry.get("ts", entry.get("timestamp", "")) + if ts: + imgui.text_colored(vec4(160, 160, 160), str(ts)) + imgui.same_line() + imgui.text_colored(C_KEY, str(kind)) + payload = entry.get("payload", entry) + text = payload.get("text", payload.get("message", payload.get("content", ""))) + if text: + preview = str(text).replace("\n", " ")[:200] + if self.ui_word_wrap: + imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(preview) + imgui.pop_text_wrap_pos() + else: + imgui.text(preview) + imgui.separator() + imgui.pop_id() + imgui.end_child() + imgui.pop_style_color() + + if not self.is_viewing_prior_session and imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): + names = self._get_discussion_names() + + if imgui.begin_combo("##disc_sel", self.active_discussion): + for name in names: + is_selected = (name == self.active_discussion) + if imgui.selectable(name, is_selected)[0]: + self._switch_discussion(name) + if is_selected: + imgui.set_item_default_focus() + imgui.end_combo() + + disc_sec = self.project.get("discussion", {}) + disc_data = disc_sec.get("discussions", {}).get(self.active_discussion, {}) + git_commit = disc_data.get("git_commit", "") + last_updated = disc_data.get("last_updated", "") + + imgui.text_colored(C_LBL, "commit:") + imgui.same_line() + imgui.text_colored(C_IN if git_commit else C_LBL, git_commit[:12] if git_commit else "(none)") + imgui.same_line() + if imgui.button("Update Commit"): + git_dir = self.ui_project_git_dir + if git_dir: + cmt = project_manager.get_git_commit(git_dir) + if cmt: + disc_data["git_commit"] = cmt + disc_data["last_updated"] = project_manager.now_ts() + self.ai_status = f"commit: {cmt[:12]}" + + imgui.text_colored(C_LBL, "updated:") + imgui.same_line() + imgui.text_colored(C_SUB, last_updated if last_updated else "(never)") + + ch, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input) + imgui.same_line() + if imgui.button("Create"): + nm = self.ui_disc_new_name_input.strip() + if nm: self._create_discussion(nm); self.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Rename"): + nm = self.ui_disc_new_name_input.strip() + if nm: self._rename_discussion(self.active_discussion, nm); self.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Delete"): + self._delete_discussion(self.active_discussion) + + if not self.is_viewing_prior_session: + imgui.separator() + if imgui.button("+ Entry"): + self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("-All"): + for e in self.disc_entries: e["collapsed"] = True + imgui.same_line() + if imgui.button("+All"): + for e in self.disc_entries: e["collapsed"] = False + imgui.same_line() + if imgui.button("Clear All"): + self.disc_entries.clear() + imgui.same_line() + if imgui.button("Save"): + self._flush_to_project() + self._save_active_project() + self._flush_to_config() + save_config(self.config) + self.ai_status = "discussion saved" + imgui.same_line() + if imgui.button("Load Log"): + self.cb_load_prior_log() + + ch, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history) + + # Truncation controls + imgui.text("Keep Pairs:") + imgui.same_line() + imgui.set_next_item_width(80) + ch, self.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", self.ui_disc_truncate_pairs, 1) + if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1 + imgui.same_line() + if imgui.button("Truncate"): + self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs) + self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs" + imgui.separator() + + if imgui.collapsing_header("Roles"): + imgui.begin_child("roles_scroll", imgui.ImVec2(0, 100), True) + for i, r in enumerate(self.disc_roles): + if imgui.button(f"x##r{i}"): + self.disc_roles.pop(i) + break + imgui.same_line() + imgui.text(r) + imgui.end_child() + ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input) + imgui.same_line() + if imgui.button("Add"): + r = self.ui_disc_new_role_input.strip() + if r and r not in self.disc_roles: + self.disc_roles.append(r) + self.ui_disc_new_role_input = "" + + imgui.separator() + imgui.begin_child("disc_scroll", imgui.ImVec2(0, 0), False) + for i, entry in enumerate(self.disc_entries): + imgui.push_id(str(i)) + collapsed = entry.get("collapsed", False) + read_mode = entry.get("read_mode", False) + + if imgui.button("+" if collapsed else "-"): + entry["collapsed"] = not collapsed + imgui.same_line() + + imgui.set_next_item_width(120) + if imgui.begin_combo("##role", entry["role"]): + for r in self.disc_roles: + if imgui.selectable(r, r == entry["role"])[0]: + entry["role"] = r + imgui.end_combo() + + if not collapsed: + imgui.same_line() + if imgui.button("[Edit]" if read_mode else "[Read]"): + entry["read_mode"] = not read_mode + + ts_str = entry.get("ts", "") + if ts_str: + imgui.same_line() + imgui.text_colored(vec4(120, 120, 100), ts_str) + + if collapsed: + imgui.same_line() + if imgui.button("Ins"): + self.disc_entries.insert(i, {"role": "User", "content": "", "collapsed": False, "ts": project_manager.now_ts()}) + imgui.same_line() + self._render_text_viewer(f"Entry #{i+1}", entry["content"]) + imgui.same_line() + if imgui.button("Del"): + self.disc_entries.pop(i) + imgui.pop_id() + break + imgui.same_line() + preview = entry["content"].replace("\n", " ")[:60] + if len(entry["content"]) > 60: preview += "..." + imgui.text_colored(vec4(160, 160, 150), preview) + + if not collapsed: + if read_mode: + imgui.begin_child("read_content", imgui.ImVec2(0, 150), True) + if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(entry["content"]) + if self.ui_word_wrap: imgui.pop_text_wrap_pos() + imgui.end_child() + else: + ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) + + imgui.separator() + imgui.pop_id() + if self._scroll_disc_to_bottom: + imgui.set_scroll_here_y(1.0) + self._scroll_disc_to_bottom = False + imgui.end_child() + + def _render_provider_panel(self): + imgui.text("Provider") + if imgui.begin_combo("##prov", self.current_provider): + for p in PROVIDERS: + if imgui.selectable(p, p == self.current_provider)[0]: + self.current_provider = p + ai_client.reset_session() + ai_client.set_provider(p, self.current_model) + self.available_models = [] + self._fetch_models(p) + imgui.end_combo() + imgui.separator() + imgui.text("Model") + imgui.same_line() + if imgui.button("Fetch Models"): + self._fetch_models(self.current_provider) + + if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): + for m in self.available_models: + if imgui.selectable(m, m == self.current_model)[0]: + self.current_model = m + ai_client.reset_session() + ai_client.set_provider(self.current_provider, m) + imgui.end_list_box() + imgui.separator() + imgui.text("Parameters") + ch, self.temperature = imgui.slider_float("Temperature", self.temperature, 0.0, 2.0, "%.2f") + ch, self.max_tokens = imgui.input_int("Max Tokens (Output)", self.max_tokens, 1024) + ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024) + + imgui.separator() + imgui.text("Telemetry") + usage = self.session_usage + total = usage["input_tokens"] + usage["output_tokens"] + imgui.text_colored(C_RES, f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})") + if usage["cache_read_input_tokens"]: + imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}") + imgui.text("Token Budget:") + imgui.progress_bar(self._token_budget_pct, imgui.ImVec2(-1, 0), f"{self._token_budget_current:,} / {self._token_budget_limit:,}") + if self._gemini_cache_text: + imgui.text_colored(C_SUB, self._gemini_cache_text) + + def _render_message_panel(self): + # LIVE indicator + is_live = self.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] + if is_live: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + imgui.text_colored(imgui.ImVec4(0.39, 1.0, 0.39, alpha), "LIVE") + imgui.separator() + + ch, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.ui_ai_input, imgui.ImVec2(-1, -40)) + + # Keyboard shortcuts + io = imgui.get_io() + ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) + ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) + if ctrl_l: + self.ui_ai_input = "" + + imgui.separator() + send_busy = False + with self._send_thread_lock: + if self.send_thread and self.send_thread.is_alive(): + send_busy = True + if imgui.button("Gen + Send") or ctrl_enter: + if not send_busy: + try: + md, path, file_items, stable_md, disc_text = self._do_generate() + self.last_md = md + self.last_md_path = path + self.last_file_items = file_items + except Exception as e: + self.ai_status = f"generate error: {e}" + else: + self.ai_status = "sending..." + user_msg = self.ui_ai_input + base_dir = self.ui_files_base_dir + csp = filter(bool, [self.ui_global_system_prompt.strip(), self.ui_project_system_prompt.strip()]) + ai_client.set_custom_system_prompt("\n\n".join(csp)) + ai_client.set_model_params(self.temperature, self.max_tokens, self.history_trunc_limit) + ai_client.set_agent_tools(self.ui_agent_tools) + send_md = stable_md + send_disc = disc_text + + def do_send(): + if self.ui_auto_add_history: + with self._pending_history_adds_lock: + self._pending_history_adds.append({"role": "User", "content": user_msg, "collapsed": False, "ts": project_manager.now_ts()}) + try: + resp = ai_client.send(send_md, user_msg, base_dir, self.last_file_items, send_disc) + self.ai_response = resp + self.ai_status = "done" + self._trigger_blink = True + if self.ui_auto_add_history: + with self._pending_history_adds_lock: + self._pending_history_adds.append({"role": "AI", "content": resp, "collapsed": False, "ts": project_manager.now_ts()}) + except ProviderError as e: + self.ai_response = e.ui_message() + self.ai_status = "error" + self._trigger_blink = True + if self.ui_auto_add_history: + with self._pending_history_adds_lock: + self._pending_history_adds.append({"role": "Vendor API", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()}) + except Exception as e: + self.ai_response = f"ERROR: {e}" + self.ai_status = "error" + self._trigger_blink = True + if self.ui_auto_add_history: + with self._pending_history_adds_lock: + self._pending_history_adds.append({"role": "System", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()}) + + with self._send_thread_lock: + self.send_thread = threading.Thread(target=do_send, daemon=True) + self.send_thread.start() + imgui.same_line() + if imgui.button("MD Only"): + try: + md, path, *_ = self._do_generate() + self.last_md = md + self.last_md_path = path + self.ai_status = f"md written: {path.name}" + except Exception as e: + self.ai_status = f"error: {e}" + imgui.same_line() + if imgui.button("Reset"): + ai_client.reset_session() + ai_client.clear_comms_log() + self._tool_log.clear() + self._comms_log.clear() + self.ai_status = "session reset" + self.ai_response = "" + imgui.same_line() + if imgui.button("-> History"): + if self.ui_ai_input: + self.disc_entries.append({"role": "User", "content": self.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) + + def _render_response_panel(self): + + if self._trigger_blink: + self._trigger_blink = False + self._is_blinking = True + self._blink_start_time = time.time() + imgui.set_window_focus_str("Response") + + if self._is_blinking: + elapsed = time.time() - self._blink_start_time + if elapsed > 1.5: + self._is_blinking = False + else: + val = math.sin(elapsed * 8 * math.pi) + alpha = 50/255 if val > 0 else 0 + imgui.push_style_color(imgui.Col_.frame_bg, vec4(0, 255, 0, alpha)) + imgui.push_style_color(imgui.Col_.child_bg, vec4(0, 255, 0, alpha)) + + if self.ui_word_wrap: + imgui.begin_child("resp_wrap", imgui.ImVec2(-1, -40), True) + imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(self.ai_response) + imgui.pop_text_wrap_pos() + imgui.end_child() + else: + imgui.input_text_multiline("##ai_out", self.ai_response, imgui.ImVec2(-1, -40), imgui.InputTextFlags_.read_only) + imgui.separator() + if imgui.button("-> History"): + if self.ai_response: + self.disc_entries.append({"role": "AI", "content": self.ai_response, "collapsed": False, "ts": project_manager.now_ts()}) + + if self._is_blinking: + imgui.pop_style_color(2) + + def _render_tool_calls_panel(self): + imgui.text("Tool call history") + imgui.same_line() + if imgui.button("Clear##tc"): + self._tool_log.clear() + imgui.separator() + imgui.begin_child("tc_scroll") + for i, (script, result) in enumerate(self._tool_log, 1): + first_line = script.strip().splitlines()[0][:80] if script.strip() else "(empty)" + imgui.text_colored(C_KEY, f"Call #{i}: {first_line}") + imgui.same_line() + self._render_text_viewer(f"Call Script #{i}", script) + imgui.same_line() + self._render_text_viewer(f"Call Output #{i}", result) + + if self.ui_word_wrap: + imgui.begin_child(f"tc_wrap_{i}", imgui.ImVec2(-1, 72), True) + imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(result) + imgui.pop_text_wrap_pos() + imgui.end_child() + else: + imgui.input_text_multiline(f"##tc_res_{i}", result, imgui.ImVec2(-1, 72), imgui.InputTextFlags_.read_only) + imgui.separator() + imgui.end_child() + + def _render_comms_history_panel(self): + imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}") + imgui.same_line() + if imgui.button("Clear##comms"): + ai_client.clear_comms_log() + self._comms_log.clear() + imgui.separator() + + imgui.text_colored(C_OUT, "OUT") + imgui.same_line() + imgui.text_colored(C_REQ, "request") + imgui.same_line() + imgui.text_colored(C_TC, "tool_call") + imgui.same_line() + imgui.text(" ") + imgui.same_line() + imgui.text_colored(C_IN, "IN") + imgui.same_line() + imgui.text_colored(C_RES, "response") + imgui.same_line() + imgui.text_colored(C_TR, "tool_result") + imgui.separator() + + imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) + for idx, entry in enumerate(self._comms_log, 1): + imgui.push_id(f"comms_{idx}") + d = entry["direction"] + k = entry["kind"] + + imgui.text_colored(vec4(160, 160, 160), f"#{idx}") + imgui.same_line() + imgui.text_colored(vec4(160, 160, 160), entry["ts"]) + imgui.same_line() + imgui.text_colored(DIR_COLORS.get(d, C_VAL), d) + imgui.same_line() + imgui.text_colored(KIND_COLORS.get(k, C_VAL), k) + imgui.same_line() + imgui.text_colored(C_LBL, f"{entry['provider']}/{entry['model']}") + + payload = entry["payload"] + + if k == "request": + self._render_heavy_text("message", payload.get("message", "")) + elif k == "response": + imgui.text_colored(C_LBL, "round:") + imgui.same_line() + imgui.text_colored(C_VAL, str(payload.get("round", ""))) + + imgui.text_colored(C_LBL, "stop_reason:") + imgui.same_line() + imgui.text_colored(vec4(255, 200, 120), str(payload.get("stop_reason", ""))) + + text = payload.get("text", "") + if text: + self._render_heavy_text("text", text) + + imgui.text_colored(C_LBL, "tool_calls:") + tcs = payload.get("tool_calls", []) + if not tcs: + imgui.text_colored(C_VAL, " (none)") + for i, tc in enumerate(tcs): + imgui.text_colored(C_KEY, f" call[{i}] {tc.get('name', '?')}") + if "id" in tc: + imgui.text_colored(C_LBL, " id:") + imgui.same_line() + imgui.text_colored(C_VAL, str(tc["id"])) + args = tc.get("args") or tc.get("input") or {} + if isinstance(args, dict): + for ak, av in args.items(): + self._render_heavy_text(f" {ak}", str(av)) + elif args: + self._render_heavy_text(" args", str(args)) + + usage = payload.get("usage") + if usage: + imgui.text_colored(C_SUB, "usage:") + for uk, uv in usage.items(): + imgui.text_colored(C_LBL, f" {uk.replace('_', ' ')}:") + imgui.same_line() + imgui.text_colored(C_NUM, str(uv)) + + elif k == "tool_call": + imgui.text_colored(C_LBL, "name:") + imgui.same_line() + imgui.text_colored(C_VAL, str(payload.get("name", ""))) + if "id" in payload: + imgui.text_colored(C_LBL, "id:") + imgui.same_line() + imgui.text_colored(C_VAL, str(payload["id"])) + if "script" in payload: + self._render_heavy_text("script", payload.get("script", "")) + elif "args" in payload: + args = payload["args"] + if isinstance(args, dict): + for ak, av in args.items(): + self._render_heavy_text(ak, str(av)) + else: + self._render_heavy_text("args", str(args)) + + elif k == "tool_result": + imgui.text_colored(C_LBL, "name:") + imgui.same_line() + imgui.text_colored(C_VAL, str(payload.get("name", ""))) + if "id" in payload: + imgui.text_colored(C_LBL, "id:") + imgui.same_line() + imgui.text_colored(C_VAL, str(payload["id"])) + self._render_heavy_text("output", payload.get("output", "")) + + elif k == "tool_result_send": + for i, r in enumerate(payload.get("results", [])): + imgui.text_colored(C_KEY, f"result[{i}]") + imgui.text_colored(C_LBL, " tool_use_id:") + imgui.same_line() + imgui.text_colored(C_VAL, str(r.get("tool_use_id", ""))) + self._render_heavy_text(" content", str(r.get("content", ""))) + else: + for key, val in payload.items(): + vstr = json.dumps(val, ensure_ascii=False, indent=2) if isinstance(val, (dict, list)) else str(val) + if key in HEAVY_KEYS: + self._render_heavy_text(key, vstr) + else: + imgui.text_colored(C_LBL, f"{key}:") + imgui.same_line() + imgui.text_colored(C_VAL, vstr) + + imgui.separator() + imgui.pop_id() + imgui.end_child() + + def _render_system_prompts_panel(self): + imgui.text("Global System Prompt (all projects)") + ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100)) + imgui.separator() + imgui.text("Project System Prompt") + ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100)) + + def _render_theme_panel(self): + imgui.text("Palette") + cp = theme.get_current_palette() + if imgui.begin_combo("##pal", cp): + for p in theme.get_palette_names(): + if imgui.selectable(p, p == cp)[0]: + theme.apply(p) + imgui.end_combo() + imgui.separator() + imgui.text("Font") + imgui.push_item_width(-150) + ch, path = imgui.input_text("##fontp", theme.get_current_font_path()) + imgui.pop_item_width() + if ch: theme._current_font_path = path + imgui.same_line() + if imgui.button("Browse##font"): + r = hide_tk_root() + p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")]) + r.destroy() + if p: theme._current_font_path = p + + imgui.text("Size (px)") + imgui.same_line() + imgui.push_item_width(100) + ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f") + if ch: theme._current_font_size = size + imgui.pop_item_width() + imgui.same_line() + if imgui.button("Apply Font (Requires Restart)"): + self._flush_to_config() + save_config(self.config) + self.ai_status = "Font settings saved. Restart required." + + imgui.separator() + imgui.text("UI Scale (DPI)") + ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") + if ch: theme.set_scale(scale) + def _load_fonts(self): font_path, font_size = theme.get_font_loading_params() if font_path and Path(font_path).exists(): diff --git a/refactor_gui2.py b/refactor_gui2.py new file mode 100644 index 0000000..a9964e7 --- /dev/null +++ b/refactor_gui2.py @@ -0,0 +1,243 @@ +import re +import sys + +def main(): + with open("gui_2.py", "r", encoding="utf-8") as f: + content = f.read() + + # 1. Update _default_windows dictionary + old_default = """ _default_windows = { + "Projects": True, + "Files": True, + "Screenshots": True, + "Discussion History": True, + "Provider": True, + "Message": True, + "Response": True, + "Tool Calls": True, + "Comms History": True, + "System Prompts": True, + "Theme": True, + "Diagnostics": False, + }""" + new_default = """ _default_windows = { + "Context Hub": True, + "AI Settings Hub": True, + "Discussion Hub": True, + "Operations Hub": True, + "Diagnostics": False, + }""" + if old_default in content: + content = content.replace(old_default, new_default) + else: + print("Could not find _default_windows block") + + # 2. Extract panels into methods + panels = { + "Projects": "_render_projects_panel", + "Files": "_render_files_panel", + "Screenshots": "_render_screenshots_panel", + "Discussion History": "_render_discussion_panel", + "Provider": "_render_provider_panel", + "Message": "_render_message_panel", + "Response": "_render_response_panel", + "Tool Calls": "_render_tool_calls_panel", + "Comms History": "_render_comms_history_panel", + "System Prompts": "_render_system_prompts_panel", + "Theme": "_render_theme_panel", + } + + methods = [] + + # We will search for: + # # ---- PanelName + # if self.show_windows["PanelName"]: + # ... (until imgui.end()) + + for panel_name, method_name in panels.items(): + # Build a regex to match the entire panel block + # We need to capture from the comment to the corresponding imgui.end() + # This requires matching balanced indentation or looking for specific end tokens. + # Since each block ends with ` imgui.end()\n`, we can use that. + # But wait, some panels like 'Response' might have different structures. + + # A simpler way: split the file by `# ---- ` comments. + pass + + # Actually, the safest way is to replace the whole `_gui_func` body from `# ---- Projects` down to just before `# ---- Diagnostics`. + start_marker = " # ---- Projects" + end_marker = " # ---- Diagnostics" + + start_idx = content.find(start_marker) + end_idx = content.find(end_marker) + + if start_idx == -1 or end_idx == -1: + print("Markers not found!") + sys.exit(1) + + panels_text = content[start_idx:end_idx] + + # Now split panels_text by `# ---- ` + panel_chunks = panels_text.split(" # ---- ") + + methods_code = "" + for chunk in panel_chunks: + if not chunk.strip(): continue + + # Find the panel name (first line) + lines = chunk.split('\n') + name = lines[0].strip() + + if name not in panels: + continue + + method_name = panels[name] + + # The rest of the lines are the panel logic. + # We need to remove the `if self.show_windows["..."]:` check and the `imgui.begin()`/`imgui.end()` calls. + # But wait! For ImGui, when we move them to child tabs, we DON'T want `imgui.begin` and `imgui.end`. + # We just want the contents inside `if exp:` + # This is critical! A tab item acts as the container. + + # Let's extract everything inside `if exp:` or just before `imgui.begin()`. + + # Find the line with `imgui.begin(` + begin_line_idx = -1 + end_line_idx = -1 + + for i, line in enumerate(lines): + if "imgui.begin(" in line: + begin_line_idx = i + elif "imgui.end()" in line: + end_line_idx = i + + if begin_line_idx == -1 or end_line_idx == -1: + print(f"Could not parse begin/end for {name}") + continue + + # Lines before begin (e.g. blinking logic in Response) + pre_begin_lines = lines[2:begin_line_idx] # skipping `if self.show_windows...:` + + # Lines between `if exp:` and `imgui.end()` + # Usually it's ` if exp:\n ...` + # We need to check if line after begin is `if exp:` + exp_check_idx = begin_line_idx + 1 + content_lines = [] + if "if exp:" in lines[exp_check_idx] or "if expanded:" in lines[exp_check_idx]: + content_lines = lines[exp_check_idx+1:end_line_idx] + else: + content_lines = lines[begin_line_idx+1:end_line_idx] + + # Post end lines (e.g. pop_style_color in Response) + # Wait, the pop_style_color is BEFORE imgui.end() + # So it's already in content_lines. + + # Reconstruct the method body + method_body = [] + + for line in pre_begin_lines: + # unindent by 12 spaces (was under `if self.show_windows...`) + if line.startswith(" "): + method_body.append(line[12:]) + else: + method_body.append(line) + + for line in content_lines: + # unindent by 16 spaces (was under `if exp:`) + if line.startswith(" "): + method_body.append(line[8:]) + elif line.startswith(" "): + # like pop_style_color which is under `if show_windows` + method_body.append(line[12:]) + else: + method_body.append(line) + + methods_code += f" def {method_name}(self):\n" + for line in method_body: + methods_code += f" {line}\n" + methods_code += "\n" + + # Hub rendering code + hub_code = """ + # ---- Context Hub + if self.show_windows.get("Context Hub", False): + exp, self.show_windows["Context Hub"] = imgui.begin("Context Hub", self.show_windows["Context Hub"]) + if exp: + if imgui.begin_tab_bar("ContextTabs"): + if imgui.begin_tab_item("Projects")[0]: + self._render_projects_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Files")[0]: + self._render_files_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Screenshots")[0]: + self._render_screenshots_panel() + imgui.end_tab_item() + imgui.end_tab_bar() + imgui.end() + + # ---- AI Settings Hub + if self.show_windows.get("AI Settings Hub", False): + exp, self.show_windows["AI Settings Hub"] = imgui.begin("AI Settings Hub", self.show_windows["AI Settings Hub"]) + if exp: + if imgui.begin_tab_bar("AISettingsTabs"): + if imgui.begin_tab_item("Provider")[0]: + self._render_provider_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("System Prompts")[0]: + self._render_system_prompts_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Theme")[0]: + self._render_theme_panel() + imgui.end_tab_item() + imgui.end_tab_bar() + imgui.end() + + # ---- Discussion Hub + if self.show_windows.get("Discussion Hub", False): + exp, self.show_windows["Discussion Hub"] = imgui.begin("Discussion Hub", self.show_windows["Discussion Hub"]) + if exp: + if imgui.begin_tab_bar("DiscussionTabs"): + if imgui.begin_tab_item("History")[0]: + self._render_discussion_panel() + imgui.end_tab_item() + imgui.end_tab_bar() + imgui.end() + + # ---- Operations Hub + if self.show_windows.get("Operations Hub", False): + exp, self.show_windows["Operations Hub"] = imgui.begin("Operations Hub", self.show_windows["Operations Hub"]) + if exp: + if imgui.begin_tab_bar("OperationsTabs"): + if imgui.begin_tab_item("Message")[0]: + self._render_message_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Response")[0]: + self._render_response_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Tool Calls")[0]: + self._render_tool_calls_panel() + imgui.end_tab_item() + if imgui.begin_tab_item("Comms History")[0]: + self._render_comms_history_panel() + imgui.end_tab_item() + imgui.end_tab_bar() + imgui.end() +""" + + # Replace panels_text with hub_code + content = content[:start_idx] + hub_code + content[end_idx:] + + # Append methods_code to the end of the App class + # We find the end of the class by looking for `def _load_fonts(self):` + # and inserting methods_code right before it. + fonts_idx = content.find(" def _load_fonts(self):") + content = content[:fonts_idx] + methods_code + content[fonts_idx:] + + with open("gui_2.py", "w", encoding="utf-8") as f: + f.write(content) + + print("Refactoring complete.") + +if __name__ == "__main__": + main() diff --git a/tests/test_gui2_layout.py b/tests/test_gui2_layout.py new file mode 100644 index 0000000..1fa475b --- /dev/null +++ b/tests/test_gui2_layout.py @@ -0,0 +1,46 @@ +import pytest +from unittest.mock import patch +from gui_2 import App + +@pytest.fixture +def app_instance(): + with ( + patch('gui_2.load_config', return_value={'gui': {'show_windows': {}}}), + patch('gui_2.save_config'), + patch('gui_2.project_manager'), + patch('gui_2.session_logger'), + patch('gui_2.immapp.run'), + patch.object(App, '_load_active_project'), + patch.object(App, '_fetch_models'), + patch.object(App, '_load_fonts'), + patch.object(App, '_post_init') + ): + yield App() + +def test_gui2_hubs_exist_in_show_windows(app_instance): + """ + Verifies that the new consolidated Hub windows are defined in the App's show_windows. + This ensures they will be available in the 'Windows' menu. + """ + expected_hubs = [ + "Context Hub", + "AI Settings Hub", + "Discussion Hub", + "Operations Hub", + ] + + for hub in expected_hubs: + assert hub in app_instance.show_windows, f"Expected hub window '{hub}' not found in show_windows" + +def test_gui2_old_windows_removed_from_show_windows(app_instance): + """ + Verifies that the old fragmented windows are removed from show_windows. + """ + old_windows = [ + "Projects", "Files", "Screenshots", + "Provider", "System Prompts", + "Message", "Response", "Tool Calls", "Comms History" + ] + + for old_win in old_windows: + assert old_win not in app_instance.show_windows, f"Old window '{old_win}' should have been removed from show_windows"