diff --git a/gui.py b/gui.py index c6c272c..87c8637 100644 --- a/gui.py +++ b/gui.py @@ -128,7 +128,8 @@ def _add_text_field(parent: str, label: str, value: str): if len(value) > COMMS_CLAMP_CHARS: if wrap: with dpg.child_window(height=80, border=True): - dpg.add_text(value, wrap=0, color=_VALUE_COLOR) + # add_input_text for selection + dpg.add_input_text(default_value=value, multiline=True, readonly=True, width=-1, height=-1, border=False) else: dpg.add_input_text( default_value=value, @@ -138,15 +139,15 @@ def _add_text_field(parent: str, label: str, value: str): height=80, ) else: - dpg.add_text(value if value else "(empty)", wrap=0, color=_VALUE_COLOR) + # Short selectable text + dpg.add_input_text(default_value=value if value else "(empty)", readonly=True, width=-1, border=False) def _add_kv_row(parent: str, key: str, val, val_color=None): """Single key: value row, horizontally laid out.""" - vc = val_color or _VALUE_COLOR with dpg.group(horizontal=True, parent=parent): dpg.add_text(f"{key}:", color=_LABEL_COLOR) - dpg.add_text(str(val), color=vc) + dpg.add_input_text(default_value=str(val), readonly=True, width=-1, border=False) def _render_usage(parent: str, usage: dict): @@ -451,6 +452,7 @@ class App: "AI Settings Hub": "win_ai_settings_hub", "Discussion Hub": "win_discussion_hub", "Operations Hub": "win_operations_hub", + "Diagnostics": "win_diagnostics", "Theme": "win_theme", "Last Script Output": "win_script_output", "Text Viewer": "win_text_viewer", @@ -488,6 +490,8 @@ class App: self._trigger_script_blink = False self._is_script_blinking = False self._script_blink_start_time = 0.0 + + self.is_viewing_prior_session = False # Subscribe to API lifecycle events ai_client.events.on("request_start", self._on_api_event) @@ -863,7 +867,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_operations_hub"): + if dpg.is_item_shown("win_diagnostics"): metrics = self.perf_monitor.get_metrics() # Update history @@ -1310,6 +1314,65 @@ class App: except Exception as e: self._update_status(f"error: {e}") + def cb_load_prior_log(self): + root = hide_tk_root() + path = filedialog.askopenfilename( + title="Load Session Log", + initialdir="logs", + filetypes=[("Log Files", "*.log"), ("JSONL Files", "*.jsonl"), ("All Files", "*.*")] + ) + root.destroy() + if not path: + return + + try: + import json + entries = [] + with open(path, "r", encoding="utf-8") as f: + for line in f: + if line.strip(): + entries.append(json.loads(line)) + + if not entries: + return + + self.is_viewing_prior_session = True + dpg.configure_item("prior_session_indicator", show=True) + dpg.configure_item("exit_prior_btn", show=True) + + # Apply Tinted Mode Theme + if not dpg.does_item_exist("prior_session_theme"): + with dpg.theme(tag="prior_session_theme"): + with dpg.theme_component(dpg.mvAll): + # Tint everything slightly amber/sepia + dpg.add_theme_color(dpg.mvThemeCol_WindowBg, (40, 30, 20, 255)) + dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (50, 40, 30, 255)) + + for hub in ["win_context_hub", "win_ai_settings_hub", "win_discussion_hub", "win_operations_hub", "win_diagnostics"]: + if dpg.does_item_exist(hub): + dpg.bind_item_theme(hub, "prior_session_theme") + + # Clear and render old entries + dpg.delete_item("comms_scroll", children_only=True) + for i, entry in enumerate(entries): + _render_comms_entry("comms_scroll", entry, i + 1) + + except Exception as e: + self._update_status(f"Load error: {e}") + + def cb_exit_prior_session(self): + self.is_viewing_prior_session = False + dpg.configure_item("prior_session_indicator", show=False) + dpg.configure_item("exit_prior_btn", show=False) + + # Unbind theme + for hub in ["win_context_hub", "win_ai_settings_hub", "win_discussion_hub", "win_operations_hub", "win_diagnostics"]: + if dpg.does_item_exist(hub): + dpg.bind_item_theme(hub, 0) + + # Restore current session comms + self._rebuild_comms_log() + def cb_reset_session(self): ai_client.reset_session() ai_client.clear_comms_log() @@ -1586,8 +1649,14 @@ class App: # ---- disc entry list ---- def _render_disc_entry(self, i: int, entry: dict): - collapsed = entry.get("collapsed", False) - read_mode = entry.get("read_mode", False) + # Default to collapsed and read-mode if not specified + if "collapsed" not in entry: + entry["collapsed"] = True + if "read_mode" not in entry: + entry["read_mode"] = True + + collapsed = entry.get("collapsed", True) + read_mode = entry.get("read_mode", True) ts_str = entry.get("ts", "") preview = entry["content"].replace("\n", " ")[:60] @@ -1602,6 +1671,11 @@ class App: width=24, callback=self._make_disc_toggle_cb(i), ) + dpg.add_button( + label="[+ Max]", + user_data=i, + callback=lambda s, a, u: _show_text_viewer(f"Entry #{u+1}", self.disc_entries[u]["content"]) + ) dpg.add_combo( tag=f"disc_role_{i}", items=self.disc_roles, @@ -1623,11 +1697,6 @@ class App: width=36, callback=self._make_disc_insert_cb(i), ) - dpg.add_button( - label="[+ Max]", - user_data=i, - callback=lambda s, a, u: _show_text_viewer(f"Entry #{u+1}", self.disc_entries[u]["content"]) - ) dpg.add_button( label="Del", width=36, @@ -1637,8 +1706,14 @@ class App: with dpg.group(tag=f"disc_body_{i}", show=not collapsed): if read_mode: - with dpg.child_window(height=150, border=True): - dpg.add_text(entry["content"], wrap=0, color=(200, 200, 200)) + # Use a read-only input_text instead of dpg.add_text to allow selection + dpg.add_input_text( + default_value=entry["content"], + multiline=True, + readonly=True, + width=-1, + height=150, + ) else: dpg.add_input_text( tag=f"disc_content_{i}", @@ -1972,6 +2047,11 @@ class App: no_close=False, no_collapse=True, ): + with dpg.group(horizontal=True): + dpg.add_text("DISCUSSION", color=_SUBHDR_COLOR) + dpg.add_spacer(width=20) + dpg.add_text("THINKING...", tag="thinking_indicator", color=(255, 100, 100), show=False) + # History at Top with dpg.child_window(tag="disc_history_section", height=-400, border=True): # Discussion selector section @@ -2008,39 +2088,35 @@ class App: with dpg.child_window(tag="disc_scroll", height=-1, border=False): pass - # Message Composer in Middle - with dpg.group(horizontal=True): - dpg.add_text("Message", color=_SUBHDR_COLOR) - dpg.add_spacer(width=20) - dpg.add_text("THINKING...", tag="thinking_indicator", color=(255, 100, 100), show=False) - - 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) + # Interaction Tabs at Bottom + with dpg.tab_bar(): + with dpg.tab(label="Message"): + dpg.add_input_text( + tag="ai_input", + multiline=True, + width=-1, + height=200, + ) + 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) + + with dpg.tab(label="AI Response"): + 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( @@ -2063,6 +2139,10 @@ class App: 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_button(label="Load Log", callback=self.cb_load_prior_log) + dpg.add_button(label="Exit Prior", tag="exit_prior_btn", callback=self.cb_exit_prior_session, show=False) + + dpg.add_text("PRIOR SESSION VIEW", tag="prior_session_indicator", color=(255, 100, 100), show=False) 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): @@ -2076,36 +2156,45 @@ class App: 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.table(header_row=False, borders_innerH=True, borders_outerH=True, borders_innerV=True, borders_outerV=True): - dpg.add_table_column() - dpg.add_table_column() - dpg.add_table_column() - dpg.add_table_column() - with dpg.table_row(): - dpg.add_text("FPS", color=_LABEL_COLOR) - dpg.add_text("0.0", tag="perf_fps_text", color=(180, 255, 180)) - dpg.add_text("Frame", color=_LABEL_COLOR) - dpg.add_text("0.0ms", tag="perf_frame_text", color=(100, 200, 255)) - with dpg.table_row(): - dpg.add_text("CPU", color=_LABEL_COLOR) - dpg.add_text("0.0%", tag="perf_cpu_text", color=(255, 220, 100)) - dpg.add_text("Lag", color=_LABEL_COLOR) - dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80)) - - dpg.add_spacer(height=4) - 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) + def _build_diagnostics_window(self): + with dpg.window( + label="Diagnostics", + tag="win_diagnostics", + pos=(1244, 804), + width=428, + height=360, + no_close=False, + no_collapse=True, + ): + dpg.add_text("Performance Telemetry") + with dpg.table(header_row=False, borders_innerH=True, borders_outerH=True, borders_innerV=True, borders_outerV=True): + dpg.add_table_column() + dpg.add_table_column() + dpg.add_table_column() + dpg.add_table_column() + with dpg.table_row(): + dpg.add_text("FPS", color=_LABEL_COLOR) + dpg.add_text("0.0", tag="perf_fps_text", color=(180, 255, 180)) + dpg.add_text("Frame", color=_LABEL_COLOR) + dpg.add_text("0.0ms", tag="perf_frame_text", color=(100, 200, 255)) + with dpg.table_row(): + dpg.add_text("CPU", color=_LABEL_COLOR) + dpg.add_text("0.0%", tag="perf_cpu_text", color=(255, 220, 100)) + dpg.add_text("Lag", color=_LABEL_COLOR) + dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80)) + + dpg.add_spacer(height=4) + dpg.add_plot(label="Frame Time (ms)", tag="plot_frame", height=140, 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) - 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) + dpg.add_plot(label="CPU Usage (%)", tag="plot_cpu", height=140, 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 @@ -2127,6 +2216,7 @@ class App: self._build_ai_settings_hub() self._build_discussion_hub() self._build_operations_hub() + self._build_diagnostics_window() self._build_theme_window() diff --git a/tests/test_layout_reorganization.py b/tests/test_layout_reorganization.py index 2a37abc..e5d6686 100644 --- a/tests/test_layout_reorganization.py +++ b/tests/test_layout_reorganization.py @@ -50,7 +50,7 @@ def test_old_windows_removed_from_window_info(app_instance_simple): "win_projects", "win_files", "win_screenshots", "win_provider", "win_system_prompts", "win_discussion", "win_message", "win_response", - "win_comms", "win_tool_log", "win_diagnostics" + "win_comms", "win_tool_log" ] for tag in old_tags: