diff --git a/gui.py b/gui.py index af32207..dda12e3 100644 --- a/gui.py +++ b/gui.py @@ -447,20 +447,13 @@ class App: self.send_thread: threading.Thread | None = None self.models_thread: threading.Thread | None = None self.window_info = { - "Projects": "win_projects", - "Files": "win_files", - "Screenshots": "win_screenshots", - "Discussion History": "win_discussion", - "Provider": "win_provider", - "Message": "win_message", - "Response": "win_response", - "Tool Calls": "win_tool_log", - "Comms History": "win_comms", - "System Prompts": "win_system_prompts", + "Context Hub": "win_context_hub", + "AI Settings Hub": "win_ai_settings_hub", + "Discussion Hub": "win_discussion_hub", + "Operations Hub": "win_operations_hub", "Theme": "win_theme", "Last Script Output": "win_script_output", "Text Viewer": "win_text_viewer", - "Diagnostics": "win_diagnostics", } @@ -870,7 +863,7 @@ class App: # Update Diagnostics panel (throttled for smoothness) if now - self._last_perf_update_time > 0.5: self._last_perf_update_time = now - if dpg.is_item_shown("win_diagnostics"): + if dpg.is_item_shown("win_operations_hub"): metrics = self.perf_monitor.get_metrics() # Update history @@ -1796,6 +1789,300 @@ class App: format="%.2f", ) + def _build_context_hub(self): + with dpg.window( + label="Context Hub", + tag="win_context_hub", + pos=(8, 8), + width=420, + height=600, + no_close=False, + ): + with dpg.tab_bar(): + with dpg.tab(label="Projects"): + proj_meta = self.project.get("project", {}) + proj_name = proj_meta.get("name", Path(self.active_project_path).stem) + dpg.add_text(f"Active: {proj_name}", tag="project_name_text", color=(140, 255, 160)) + dpg.add_separator() + dpg.add_text("Git Directory") + with dpg.group(horizontal=True): + dpg.add_input_text( + tag="project_git_dir", + default_value=proj_meta.get("git_dir", ""), + width=-100, + ) + dpg.add_button(label="Browse##git", callback=self.cb_browse_git_dir) + dpg.add_separator() + dpg.add_text("Main Context File") + with dpg.group(horizontal=True): + dpg.add_input_text( + tag="project_main_context", + default_value=proj_meta.get("main_context", ""), + width=-100, + ) + dpg.add_button(label="Browse##ctx", callback=self.cb_browse_main_context) + dpg.add_separator() + dpg.add_text("Output Dir") + with dpg.group(horizontal=True): + dpg.add_input_text( + tag="output_dir", + default_value=self.project.get("output", {}).get("output_dir", "./md_gen"), + width=-100, + ) + dpg.add_button(label="Browse##out", callback=self.cb_browse_output) + dpg.add_separator() + dpg.add_text("Project Files") + with dpg.child_window(tag="projects_scroll", height=120, border=True): + pass + with dpg.group(horizontal=True): + dpg.add_button(label="Add Project", callback=self.cb_add_project) + dpg.add_button(label="New Project", callback=self.cb_new_project) + dpg.add_button(label="Save All", callback=self.cb_save_config) + dpg.add_checkbox( + tag="project_word_wrap", + label="Word-Wrap (Read-only panels)", + default_value=self.project.get("project", {}).get("word_wrap", True), + callback=self.cb_word_wrap_toggled + ) + dpg.add_separator() + dpg.add_text("Agent Capabilities") + agent_tools = self.project.get("agent", {}).get("tools", {}) + for t_name in ["run_powershell", "read_file", "list_directory", "search_files", "get_file_summary", "web_search", "fetch_url"]: + dpg.add_checkbox( + tag=f"tool_toggle_{t_name}", + label=f"Enable {t_name}", + default_value=agent_tools.get(t_name, True) + ) + + with dpg.tab(label="Files"): + dpg.add_text("Base Dir") + with dpg.group(horizontal=True): + dpg.add_input_text( + tag="files_base_dir", + default_value=self.project.get("files", {}).get("base_dir", "."), + width=-100, + ) + dpg.add_button( + label="Browse##filesbase", callback=self.cb_browse_files_base + ) + dpg.add_separator() + dpg.add_text("Paths") + with dpg.child_window(tag="files_scroll", height=-64, border=True): + pass + dpg.add_separator() + with dpg.group(horizontal=True): + dpg.add_button(label="Add File(s)", callback=self.cb_add_files) + dpg.add_button(label="Add Wildcard", callback=self.cb_add_wildcard) + + with dpg.tab(label="Screenshots"): + dpg.add_text("Base Dir") + with dpg.group(horizontal=True): + dpg.add_input_text( + tag="shots_base_dir", + default_value=self.project.get("screenshots", {}).get("base_dir", "."), + width=-100, + ) + dpg.add_button( + label="Browse##shotsbase", callback=self.cb_browse_shots_base + ) + dpg.add_separator() + dpg.add_text("Paths") + with dpg.child_window(tag="shots_scroll", height=-48, border=True): + pass + dpg.add_separator() + dpg.add_button(label="Add Screenshot(s)", callback=self.cb_add_shots) + + def _build_ai_settings_hub(self): + with dpg.window( + label="AI Settings Hub", + tag="win_ai_settings_hub", + pos=(8, 616), + width=420, + height=556, + no_close=False, + ): + with dpg.collapsing_header(label="Provider & Models", default_open=True): + dpg.add_text("Provider") + dpg.add_combo( + tag="provider_combo", + items=PROVIDERS, + default_value=self.current_provider, + width=-1, + callback=self.cb_provider_changed, + ) + with dpg.group(horizontal=True): + dpg.add_text("Model") + dpg.add_button(label="Fetch Models", callback=self.cb_fetch_models) + dpg.add_listbox( + tag="model_listbox", + items=self.available_models, + default_value=self.current_model, + width=-1, + num_items=5, + callback=self.cb_model_changed, + ) + dpg.add_separator() + dpg.add_text("Telemetry") + dpg.add_text("History Token Budget:", color=_LABEL_COLOR) + dpg.add_progress_bar(tag="token_budget_bar", default_value=0.0, width=-1) + dpg.add_text("0 / 0", tag="token_budget_label") + dpg.add_text("", tag="gemini_cache_label", show=False) + + with dpg.collapsing_header(label="Parameters", default_open=True): + dpg.add_input_float(tag="ai_temperature", label="Temperature", default_value=self.temperature, min_value=0.0, max_value=2.0) + dpg.add_input_int(tag="ai_max_tokens", label="Max Tokens (Output)", default_value=self.max_tokens, step=1024) + dpg.add_input_int(tag="ai_history_trunc", label="History Truncation Limit", default_value=self.history_trunc_limit, step=1024) + + with dpg.collapsing_header(label="System Prompts", default_open=False): + dpg.add_text("Global System Prompt") + dpg.add_input_text( + tag="global_system_prompt", + default_value=self.config.get("ai", {}).get("system_prompt", ""), + multiline=True, + width=-1, + height=100, + ) + dpg.add_separator() + dpg.add_text("Project System Prompt") + dpg.add_input_text( + tag="project_system_prompt", + default_value=self.project.get("project", {}).get("system_prompt", ""), + multiline=True, + width=-1, + height=100, + ) + + def _build_discussion_hub(self): + with dpg.window( + label="Discussion Hub", + tag="win_discussion_hub", + pos=(436, 8), + width=800, + height=1164, + no_close=False, + ): + # History at Top + with dpg.child_window(tag="disc_history_section", height=-400, border=True): + # Discussion selector section + with dpg.collapsing_header(label="Discussions", default_open=False): + with dpg.group(tag="disc_selector_group"): + pass # populated by _rebuild_discussion_selector + + dpg.add_separator() + + # Entry toolbar + with dpg.group(horizontal=True): + dpg.add_button(label="+ Entry", callback=self.cb_disc_append_entry) + dpg.add_button(label="-All", callback=self.cb_disc_collapse_all) + dpg.add_button(label="+All", callback=self.cb_disc_expand_all) + dpg.add_text("Keep Pairs:", color=(160, 160, 160)) + dpg.add_input_int(tag="disc_truncate_pairs", default_value=2, width=80, min_value=1) + dpg.add_button(label="Truncate", callback=self.cb_disc_truncate) + dpg.add_button(label="Clear All", callback=self.cb_disc_clear) + dpg.add_button(label="Save", callback=self.cb_disc_save) + + dpg.add_checkbox( + tag="auto_add_history", + label="Auto-add message & response to history", + default_value=self.project.get("discussion", {}).get("auto_add", False) + ) + dpg.add_separator() + with dpg.collapsing_header(label="Roles", default_open=False): + with dpg.child_window(tag="disc_roles_scroll", height=96, border=True): + pass + with dpg.group(horizontal=True): + dpg.add_input_text(tag="disc_new_role_input", hint="New role name", width=-72) + dpg.add_button(label="Add", callback=self.cb_disc_add_role) + dpg.add_separator() + with dpg.child_window(tag="disc_scroll", height=-1, border=False): + pass + + # Message Composer in Middle + dpg.add_text("Message", color=_SUBHDR_COLOR) + dpg.add_input_text( + tag="ai_input", + multiline=True, + width=-1, + height=120, + ) + with dpg.group(horizontal=True): + dpg.add_button(label="Gen + Send", callback=self.cb_generate_send) + dpg.add_button(label="MD Only", callback=self.cb_md_only) + dpg.add_button(label="Reset", callback=self.cb_reset_session) + dpg.add_button(label="-> History", callback=self.cb_append_message_to_history) + + dpg.add_separator() + + # AI Response at Bottom + dpg.add_text("AI Response", color=_SUBHDR_COLOR) + dpg.add_input_text( + tag="ai_response", + multiline=True, + readonly=True, + width=-1, + height=-48, + ) + with dpg.child_window(tag="ai_response_wrap_container", width=-1, height=-48, border=True, show=False): + dpg.add_text("", tag="ai_response_wrap", wrap=0) + dpg.add_separator() + dpg.add_button(label="-> History", callback=self.cb_append_response_to_history) + + def _build_operations_hub(self): + with dpg.window( + label="Operations Hub", + tag="win_operations_hub", + pos=(1244, 8), + width=428, + height=1164, + no_close=False, + ): + with dpg.tab_bar(): + with dpg.tab(label="Comms Log"): + with dpg.group(horizontal=True): + dpg.add_text("Status: idle", tag="ai_status", color=(200, 220, 160)) + dpg.add_spacer(width=16) + dpg.add_button(label="Clear", callback=self.cb_clear_comms) + dpg.add_text("Tokens: 0 (In: 0 Out: 0)", tag="ai_token_usage", color=(180, 255, 180)) + dpg.add_separator() + with dpg.child_window(tag="comms_scroll", height=-1, border=False, horizontal_scrollbar=True): + pass + + with dpg.tab(label="Tool Log"): + with dpg.group(horizontal=True): + dpg.add_text("Tool call history") + dpg.add_button(label="Clear", callback=self.cb_clear_tool_log) + dpg.add_separator() + with dpg.child_window(tag="tool_log_scroll", height=-1, border=False): + pass + + with dpg.tab(label="Diagnostics"): + dpg.add_text("Performance Telemetry") + with dpg.group(horizontal=True): + dpg.add_text("FPS:") + dpg.add_text("0.0", tag="perf_fps_text", color=(180, 255, 180)) + dpg.add_spacer(width=20) + dpg.add_text("Frame:") + dpg.add_text("0.0ms", tag="perf_frame_text", color=(100, 200, 255)) + + dpg.add_plot(label="Frame Time (ms)", tag="plot_frame", height=120, width=-1, no_mouse_pos=True) + dpg.add_plot_axis(dpg.mvXAxis, label="samples", no_tick_labels=True, parent="plot_frame") + with dpg.plot_axis(dpg.mvYAxis, label="ms", tag="axis_frame_y", parent="plot_frame"): + dpg.add_line_series(list(range(100)), self.perf_history["frame_time"], label="frame time", tag="perf_frame_plot") + dpg.set_axis_limits("axis_frame_y", 0, 50) + + with dpg.group(horizontal=True): + dpg.add_text("CPU:") + dpg.add_text("0.0%", tag="perf_cpu_text", color=(255, 220, 100)) + dpg.add_spacer(width=20) + dpg.add_text("Input Lag:") + dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80)) + + dpg.add_plot(label="CPU Usage (%)", tag="plot_cpu", height=120, width=-1, no_mouse_pos=True) + dpg.add_plot_axis(dpg.mvXAxis, label="samples", no_tick_labels=True, parent="plot_cpu") + with dpg.plot_axis(dpg.mvYAxis, label="%", tag="axis_cpu_y", parent="plot_cpu"): + dpg.add_line_series(list(range(100)), self.perf_history["cpu"], label="cpu usage", tag="perf_cpu_plot") + dpg.set_axis_limits("axis_cpu_y", 0, 100) + def _build_ui(self): # Performance tracking handlers with dpg.handler_registry(): @@ -1811,325 +2098,11 @@ class App: dpg.add_menu_item(label="Reset Session", callback=self.cb_reset_session) dpg.add_menu_item(label="Generate MD Only", callback=self.cb_md_only) - - # ---- Projects panel ---- - with dpg.window( - label="Projects", - tag="win_projects", - pos=(8, 8), - width=400, - height=380, - no_close=False, - ): - proj_meta = self.project.get("project", {}) - proj_name = proj_meta.get("name", Path(self.active_project_path).stem) - dpg.add_text(f"Active: {proj_name}", tag="project_name_text", color=(140, 255, 160)) - dpg.add_separator() - dpg.add_text("Git Directory") - with dpg.group(horizontal=True): - dpg.add_input_text( - tag="project_git_dir", - default_value=proj_meta.get("git_dir", ""), - width=-100, - ) - dpg.add_button(label="Browse##git", callback=self.cb_browse_git_dir) - dpg.add_separator() - dpg.add_text("Main Context File") - with dpg.group(horizontal=True): - dpg.add_input_text( - tag="project_main_context", - default_value=proj_meta.get("main_context", ""), - width=-100, - ) - dpg.add_button(label="Browse##ctx", callback=self.cb_browse_main_context) - dpg.add_separator() - dpg.add_text("Output Dir") - with dpg.group(horizontal=True): - dpg.add_input_text( - tag="output_dir", - default_value=self.project.get("output", {}).get("output_dir", "./md_gen"), - width=-100, - ) - dpg.add_button(label="Browse##out", callback=self.cb_browse_output) - dpg.add_separator() - dpg.add_text("Project Files") - with dpg.child_window(tag="projects_scroll", height=-60, border=True): - pass - with dpg.group(horizontal=True): - dpg.add_button(label="Add Project", callback=self.cb_add_project) - dpg.add_button(label="New Project", callback=self.cb_new_project) - dpg.add_button(label="Save All", callback=self.cb_save_config) - dpg.add_checkbox( - tag="project_word_wrap", - label="Word-Wrap (Read-only panels)", - default_value=self.project.get("project", {}).get("word_wrap", True), - callback=self.cb_word_wrap_toggled - ) - dpg.add_separator() - dpg.add_text("Agent Capabilities") - agent_tools = self.project.get("agent", {}).get("tools", {}) - for t_name in ["run_powershell", "read_file", "list_directory", "search_files", "get_file_summary", "web_search", "fetch_url"]: - dpg.add_checkbox( - tag=f"tool_toggle_{t_name}", - label=f"Enable {t_name}", - default_value=agent_tools.get(t_name, True) - ) - - # ---- Files panel ---- - with dpg.window( - label="Files", - tag="win_files", - pos=(8, 396), - width=400, - height=360, - no_close=False, - ): - dpg.add_text("Base Dir") - with dpg.group(horizontal=True): - dpg.add_input_text( - tag="files_base_dir", - default_value=self.project.get("files", {}).get("base_dir", "."), - width=-220, - ) - dpg.add_button( - label="Browse##filesbase", callback=self.cb_browse_files_base - ) - dpg.add_separator() - dpg.add_text("Paths") - with dpg.child_window(tag="files_scroll", height=-64, border=True): - pass - dpg.add_separator() - with dpg.group(horizontal=True): - dpg.add_button(label="Add File(s)", callback=self.cb_add_files) - dpg.add_button(label="Add Wildcard", callback=self.cb_add_wildcard) - - # ---- Screenshots panel ---- - with dpg.window( - label="Screenshots", - tag="win_screenshots", - pos=(416, 8), - width=400, - height=500, - no_close=False, - ): - dpg.add_text("Base Dir") - with dpg.group(horizontal=True): - dpg.add_input_text( - tag="shots_base_dir", - default_value=self.project.get("screenshots", {}).get("base_dir", "."), - width=-220, - ) - dpg.add_button( - label="Browse##shotsbase", callback=self.cb_browse_shots_base - ) - dpg.add_separator() - dpg.add_text("Paths") - with dpg.child_window(tag="shots_scroll", height=-48, border=True): - pass - self._rebuild_shots_list() - dpg.add_separator() - dpg.add_button(label="Add Screenshot(s)", callback=self.cb_add_shots) - - # ---- Discussion History panel ---- - with dpg.window( - label="Discussion History", - tag="win_discussion", - pos=(824, 8), - width=420, - height=600, - no_close=False, - ): - # Discussion selector section - with dpg.collapsing_header(label="Discussions", default_open=True): - with dpg.group(tag="disc_selector_group"): - pass # populated by _rebuild_discussion_selector - - dpg.add_separator() - - # Entry toolbar - with dpg.group(horizontal=True): - dpg.add_button(label="+ Entry", callback=self.cb_disc_append_entry) - dpg.add_button(label="-All", callback=self.cb_disc_collapse_all) - dpg.add_button(label="+All", callback=self.cb_disc_expand_all) - dpg.add_text("Keep Pairs:", color=(160, 160, 160)) - dpg.add_input_int(tag="disc_truncate_pairs", default_value=2, width=120, min_value=1) - dpg.add_button(label="Truncate", callback=self.cb_disc_truncate) - dpg.add_button(label="Clear All", callback=self.cb_disc_clear) - dpg.add_button(label="Save", callback=self.cb_disc_save) - dpg.add_checkbox( - tag="auto_add_history", - label="Auto-add message & response to history", - default_value=self.project.get("discussion", {}).get("auto_add", False) - ) - dpg.add_separator() - with dpg.collapsing_header(label="Roles", default_open=False): - with dpg.child_window(tag="disc_roles_scroll", height=96, border=True): - pass - with dpg.group(horizontal=True): - dpg.add_input_text( - tag="disc_new_role_input", - hint="New role name", - width=-72, - ) - dpg.add_button(label="Add", callback=self.cb_disc_add_role) - dpg.add_separator() - with dpg.child_window(tag="disc_scroll", height=-1, border=False): - pass - - # ---- Provider panel ---- - with dpg.window( - label="Provider", - tag="win_provider", - pos=(1252, 8), - width=420, - height=260, - no_close=False, - ): - dpg.add_text("Provider") - dpg.add_combo( - tag="provider_combo", - items=PROVIDERS, - default_value=self.current_provider, - width=-1, - callback=self.cb_provider_changed, - ) - dpg.add_separator() - with dpg.group(horizontal=True): - dpg.add_text("Model") - dpg.add_button(label="Fetch Models", callback=self.cb_fetch_models) - dpg.add_listbox( - tag="model_listbox", - items=self.available_models, - default_value=self.current_model, - width=-1, - num_items=5, - callback=self.cb_model_changed, - ) - dpg.add_separator() - dpg.add_text("Telemetry") - dpg.add_text("History Token Budget:", color=_LABEL_COLOR) - dpg.add_progress_bar(tag="token_budget_bar", default_value=0.0, width=-1) - dpg.add_text("0 / 0", tag="token_budget_label") - dpg.add_text("", tag="gemini_cache_label", show=False) - dpg.add_separator() - dpg.add_text("Parameters") - dpg.add_input_float(tag="ai_temperature", label="Temperature", default_value=self.temperature, min_value=0.0, max_value=2.0) - dpg.add_input_int(tag="ai_max_tokens", label="Max Tokens (Output)", default_value=self.max_tokens, step=1024) - dpg.add_input_int(tag="ai_history_trunc", label="History Truncation Limit", default_value=self.history_trunc_limit, step=1024) - - # ---- Message panel ---- - with dpg.window( - label="Message", - tag="win_message", - pos=(1252, 276), - width=420, - height=280, - no_close=False, - ): - dpg.add_input_text( - tag="ai_input", - multiline=True, - width=-1, - height=-64, - ) - dpg.add_separator() - with dpg.group(horizontal=True): - dpg.add_button(label="Gen + Send", callback=self.cb_generate_send) - dpg.add_button(label="MD Only", callback=self.cb_md_only) - dpg.add_button(label="Reset", callback=self.cb_reset_session) - dpg.add_button(label="-> History", callback=self.cb_append_message_to_history) - - # ---- Response panel ---- - with dpg.window( - label="Response", - tag="win_response", - pos=(1252, 564), - width=420, - height=300, - no_close=False, - ): - dpg.add_input_text( - tag="ai_response", - multiline=True, - readonly=True, - width=-1, - height=-48, - ) - with dpg.child_window(tag="ai_response_wrap_container", width=-1, height=-48, border=True, show=False): - dpg.add_text("", tag="ai_response_wrap", wrap=0) - dpg.add_separator() - dpg.add_button(label="-> History", callback=self.cb_append_response_to_history) - - # ---- Tool Calls panel ---- - with dpg.window( - label="Tool Calls", - tag="win_tool_log", - pos=(1252, 872), - width=420, - height=300, - no_close=False, - ): - with dpg.group(horizontal=True): - dpg.add_text("Tool call history") - dpg.add_button(label="Clear", callback=self.cb_clear_tool_log) - dpg.add_separator() - with dpg.child_window(tag="tool_log_scroll", height=-1, border=False): - pass - - # ---- Comms History panel ---- - with dpg.window( - label="Comms History", - tag="win_comms", - pos=(1680, 8), - width=520, - height=1164, - no_close=False, - ): - with dpg.group(horizontal=True): - dpg.add_text("Status: idle", tag="ai_status", color=(200, 220, 160)) - dpg.add_spacer(width=16) - dpg.add_text("Tokens: 0 (In: 0 Out: 0)", tag="ai_token_usage", color=(180, 255, 180)) - dpg.add_spacer(width=16) - dpg.add_button(label="Clear", callback=self.cb_clear_comms) - dpg.add_separator() - with dpg.group(horizontal=True): - dpg.add_text("OUT", color=_DIR_COLORS["OUT"]) - dpg.add_text("request", color=_KIND_COLORS["request"]) - dpg.add_text("tool_call", color=_KIND_COLORS["tool_call"]) - dpg.add_spacer(width=8) - dpg.add_text("IN", color=_DIR_COLORS["IN"]) - dpg.add_text("response", color=_KIND_COLORS["response"]) - dpg.add_text("tool_result", color=_KIND_COLORS["tool_result"]) - dpg.add_separator() - with dpg.child_window(tag="comms_scroll", height=-1, border=False, horizontal_scrollbar=True): - pass - - # ---- System Prompts panel ---- - with dpg.window( - label="System Prompts", - tag="win_system_prompts", - pos=(416, 804), - width=400, - height=300, - no_close=False, - ): - dpg.add_text("Global System Prompt (all projects)") - dpg.add_input_text( - tag="global_system_prompt", - default_value=self.config.get("ai", {}).get("system_prompt", ""), - multiline=True, - width=-1, - height=100, - ) - dpg.add_separator() - dpg.add_text("Project System Prompt") - dpg.add_input_text( - tag="project_system_prompt", - default_value=self.project.get("project", {}).get("system_prompt", ""), - multiline=True, - width=-1, - height=100, - ) + # Build Hubs + self._build_context_hub() + self._build_ai_settings_hub() + self._build_discussion_hub() + self._build_operations_hub() self._build_theme_window() @@ -2195,42 +2168,6 @@ class App: with dpg.child_window(tag="text_viewer_wrap_container", width=-1, height=-1, border=False, show=False): dpg.add_text("", tag="text_viewer_wrap", wrap=0) - # ---- Diagnostics panel ---- - with dpg.window( - label="Diagnostics", - tag="win_diagnostics", - pos=(8, 804), - width=400, - height=380, - no_close=False, - ): - dpg.add_text("Performance Telemetry") - with dpg.group(horizontal=True): - dpg.add_text("FPS:") - dpg.add_text("0.0", tag="perf_fps_text", color=(180, 255, 180)) - dpg.add_spacer(width=20) - dpg.add_text("Frame:") - dpg.add_text("0.0ms", tag="perf_frame_text", color=(100, 200, 255)) - - dpg.add_plot(label="Frame Time (ms)", tag="plot_frame", height=100, width=-1, no_mouse_pos=True) - dpg.add_plot_axis(dpg.mvXAxis, label="samples", no_tick_labels=True, parent="plot_frame") - with dpg.plot_axis(dpg.mvYAxis, label="ms", tag="axis_frame_y", parent="plot_frame"): - dpg.add_line_series(list(range(100)), self.perf_history["frame_time"], label="frame time", tag="perf_frame_plot") - dpg.set_axis_limits("axis_frame_y", 0, 50) - - with dpg.group(horizontal=True): - dpg.add_text("CPU:") - dpg.add_text("0.0%", tag="perf_cpu_text", color=(255, 220, 100)) - dpg.add_spacer(width=20) - dpg.add_text("Input Lag:") - dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80)) - - dpg.add_plot(label="CPU Usage (%)", tag="plot_cpu", height=100, width=-1, no_mouse_pos=True) - dpg.add_plot_axis(dpg.mvXAxis, label="samples", no_tick_labels=True, parent="plot_cpu") - with dpg.plot_axis(dpg.mvYAxis, label="%", tag="axis_cpu_y", parent="plot_cpu"): - dpg.add_line_series(list(range(100)), self.perf_history["cpu"], label="cpu usage", tag="perf_cpu_plot") - dpg.set_axis_limits("axis_cpu_y", 0, 100) - def _process_pending_gui_tasks(self): """Processes tasks queued from background threads on the main thread.""" if not self._pending_gui_tasks: @@ -2368,8 +2305,8 @@ class App: self._trigger_blink = False self._is_blinking = True self._blink_start_time = time.time() - if dpg.does_item_exist("win_response"): - dpg.focus_item("win_response") + if dpg.does_item_exist("win_discussion_hub"): + dpg.focus_item("win_discussion_hub") if self._is_blinking: elapsed = time.time() - self._blink_start_time diff --git a/tests/test_gui_diagnostics.py b/tests/test_gui_diagnostics.py index 93dd7bc..83dcf2d 100644 --- a/tests/test_gui_diagnostics.py +++ b/tests/test_gui_diagnostics.py @@ -31,8 +31,8 @@ def app_instance(): dpg.destroy_context() def test_diagnostics_panel_initialization(app_instance): - assert "Diagnostics" in app_instance.window_info - assert app_instance.window_info["Diagnostics"] == "win_diagnostics" + assert "Operations Hub" in app_instance.window_info + assert app_instance.window_info["Operations Hub"] == "win_operations_hub" assert "frame_time" in app_instance.perf_history assert len(app_instance.perf_history["frame_time"]) == 100 diff --git a/tests/test_layout_reorganization.py b/tests/test_layout_reorganization.py new file mode 100644 index 0000000..1355166 --- /dev/null +++ b/tests/test_layout_reorganization.py @@ -0,0 +1,61 @@ +import pytest +import sys +import os +import importlib.util + +# Ensure project root is in path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +# Load gui.py +spec = importlib.util.spec_from_file_location("gui", "gui.py") +gui = importlib.util.module_from_spec(spec) +sys.modules["gui"] = gui +spec.loader.exec_module(gui) +from gui import App + +def test_new_hubs_defined_in_window_info(): + """ + Verifies that the new consolidated Hub windows are defined in the App's window_info. + This ensures they will be available in the 'Windows' menu. + """ + # We don't need a full App instance with DPG context for this, + # as window_info is initialized in __init__ before DPG starts. + # But we mock load_config to avoid file access. + from unittest.mock import patch + with patch('gui.load_config', return_value={}): + app = App() + + expected_hubs = { + "Context Hub": "win_context_hub", + "AI Settings Hub": "win_ai_settings_hub", + "Discussion Hub": "win_discussion_hub", + "Operations Hub": "win_operations_hub", + } + + for label, tag in expected_hubs.items(): + assert tag in app.window_info.values(), f"Expected window tag {tag} not found in window_info" + # Check if the label matches (or is present) + found = False + for l, t in app.window_info.items(): + if t == tag: + found = True + assert l == label or label in l, f"Label mismatch for {tag}: expected {label}, found {l}" + assert found, f"Expected window label {label} not found in window_info" + +def test_old_windows_removed_from_window_info(): + """ + Verifies that the old fragmented windows are removed from window_info. + """ + from unittest.mock import patch + with patch('gui.load_config', return_value={}): + app = App() + + old_tags = [ + "win_projects", "win_files", "win_screenshots", + "win_provider", "win_system_prompts", + "win_discussion", "win_message", "win_response", + "win_comms", "win_tool_log", "win_diagnostics" + ] + + for tag in old_tags: + assert tag not in app.window_info.values(), f"Old window tag {tag} should have been removed from window_info"