From caa67206faea0b3a0732761e44ca6afc3bafc7f5 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 21 Feb 2026 23:08:42 -0500 Subject: [PATCH] ux improvements --- MainContext.md | 6 ++ aggregate.py | 4 +- gui.py | 232 +++++++++++++++++++++++++++++++-------------- manual_slop.toml | 10 +- project_manager.py | 3 +- 5 files changed, 181 insertions(+), 74 deletions(-) diff --git a/MainContext.md b/MainContext.md index f4f15b9..6c0dbab 100644 --- a/MainContext.md +++ b/MainContext.md @@ -199,3 +199,9 @@ Entry layout: index + timestamp + direction + kind + provider/model header row, ### Gemini Context Management - Investigating ways to prevent context duplication in _gemini_chat history, as currently {md_content} is prepended to the user message on every single request, causing history bloat. - Discussing explicit Gemini Context Caching API (client.caches.create()) to store read-only file context and avoid re-reading files across sessions. + +### Latest Changes +- Removed `Config` panel from the GUI to streamline per-project configuration. +- `output_dir` was moved into the Projects panel. +- `auto_add_history` was moved to the Discussion History panel. +- `namespace` is no longer a configurable field; `aggregate.py` automatically uses the active project's `name` property. diff --git a/aggregate.py b/aggregate.py index 4fb8a62..f64ea90 100644 --- a/aggregate.py +++ b/aggregate.py @@ -128,7 +128,9 @@ def build_markdown(base_dir: Path, files: list[str], screenshot_base_dir: Path, return "\n\n---\n\n".join(parts) def run(config: dict) -> tuple[str, Path]: - namespace = config["output"]["namespace"] + namespace = config.get("project", {}).get("name") + if not namespace: + namespace = config.get("output", {}).get("namespace", "project") output_dir = Path(config["output"]["output_dir"]) base_dir = Path(config["files"]["base_dir"]) files = config["files"].get("paths", []) diff --git a/gui.py b/gui.py index 7934326..8a31880 100644 --- a/gui.py +++ b/gui.py @@ -3,6 +3,8 @@ import dearpygui.dearpygui as dpg import tomllib import tomli_w import threading +import time +import math from pathlib import Path from tkinter import filedialog, Tk import aggregate @@ -239,54 +241,71 @@ class ConfirmDialog: ConfirmDialog._next_id += 1 self._uid = ConfirmDialog._next_id self._tag = f"confirm_dlg_{self._uid}" - self._script = script - self._base_dir = base_dir + # Cast to str to ensure DPG doesn't crash on None or weird objects + self._script = str(script) if script is not None else "" + self._base_dir = str(base_dir) if base_dir is not None else "" self._event = threading.Event() self._approved = False def show(self): - """Called from main thread only.""" - w, h = 700, 440 - vp_w = dpg.get_viewport_width() - vp_h = dpg.get_viewport_height() - px = max(0, (vp_w - w) // 2) - py = max(0, (vp_h - h) // 2) + """Called from main thread only. Wrapped in try/except to prevent thread lockups.""" + try: + w, h = 700, 440 + vp_w = dpg.get_viewport_width() + vp_h = dpg.get_viewport_height() + px = max(0, (vp_w - w) // 2) + py = max(0, (vp_h - h) // 2) - with dpg.window( - label=f"Approve PowerShell Command #{self._uid}", - tag=self._tag, - modal=True, - no_close=True, - pos=(px, py), - width=w, - height=h, - ): - dpg.add_text("The AI wants to run the following PowerShell script:") - dpg.add_text(f"base_dir: {self._base_dir}", color=(200, 200, 100)) - dpg.add_separator() - dpg.add_input_text( - tag=f"{self._tag}_script", - default_value=self._script, - multiline=True, - width=-1, - height=-72, - readonly=False, - ) - dpg.add_separator() - with dpg.group(horizontal=True): - dpg.add_button(label="Approve & Run", callback=self._cb_approve) - dpg.add_button(label="Reject", callback=self._cb_reject) + with dpg.window( + label=f"Approve PowerShell Command #{self._uid}", + tag=self._tag, + modal=True, + no_close=True, + pos=(px, py), + width=w, + height=h, + ): + dpg.add_text("The AI wants to run the following PowerShell script:") + dpg.add_text(f"base_dir: {self._base_dir}", color=(200, 200, 100)) + dpg.add_separator() + dpg.add_input_text( + tag=f"{self._tag}_script", + default_value=self._script, + multiline=True, + width=-1, + height=-72, + readonly=False, + ) + dpg.add_separator() + with dpg.group(horizontal=True): + dpg.add_button(label="Approve & Run", callback=self._cb_approve) + dpg.add_button(label="Reject", callback=self._cb_reject) + + dpg.focus_item(self._tag) + except Exception as e: + print(f"ERROR rendering ConfirmDialog: {e}") + self._approved = False + self._event.set() def _cb_approve(self): - self._script = dpg.get_value(f"{self._tag}_script") + try: + self._script = dpg.get_value(f"{self._tag}_script") + except Exception: + pass self._approved = True self._event.set() - dpg.delete_item(self._tag) + try: + dpg.delete_item(self._tag) + except Exception: + pass def _cb_reject(self): self._approved = False self._event.set() - dpg.delete_item(self._tag) + try: + dpg.delete_item(self._tag) + except Exception: + pass def wait(self) -> tuple[bool, str]: """Called from background thread. Blocks until user acts.""" @@ -362,6 +381,15 @@ class App: self._pending_comms: list[dict] = [] self._pending_comms_lock = threading.Lock() self._comms_entry_count = 0 + + # Auto-history queues + self._pending_history_adds: list[dict] = [] + self._pending_history_adds_lock = threading.Lock() + + # Blink state + self._trigger_blink = False + self._is_blinking = False + self._blink_start_time = 0.0 session_logger.open_session() ai_client.set_provider(self.current_provider, self.current_model) @@ -452,8 +480,6 @@ class App: def _refresh_project_widgets(self): """Push project-level values into the GUI widgets.""" proj = self.project - if dpg.does_item_exist("namespace"): - dpg.set_value("namespace", proj.get("output", {}).get("namespace", "")) if dpg.does_item_exist("output_dir"): dpg.set_value("output_dir", proj.get("output", {}).get("output_dir", "./md_gen")) if dpg.does_item_exist("files_base_dir"): @@ -469,6 +495,8 @@ class App: dpg.set_value("project_system_prompt", proj.get("project", {}).get("system_prompt", "")) if dpg.does_item_exist("project_main_context"): dpg.set_value("project_main_context", proj.get("project", {}).get("main_context", "")) + if dpg.does_item_exist("auto_add_history"): + dpg.set_value("auto_add_history", proj.get("discussion", {}).get("auto_add", False)) def _save_active_project(self): """Write self.project to the active project .toml file.""" @@ -586,6 +614,16 @@ class App: self._rebuild_discussion_selector() self._update_status(f"commit: {commit[:12]}") + def _queue_history_add(self, role: str, content: str): + """Safely queue a new history entry from a background thread.""" + with self._pending_history_adds_lock: + self._pending_history_adds.append({ + "role": role, + "content": content, + "collapsed": False, + "ts": project_manager.now_ts() + }) + # ---------------------------------------------------------------- comms log def _on_comms_entry(self, entry: dict): @@ -671,8 +709,6 @@ class App: # Output proj.setdefault("output", {}) - if dpg.does_item_exist("namespace"): - proj["output"]["namespace"] = dpg.get_value("namespace") if dpg.does_item_exist("output_dir"): proj["output"]["output_dir"] = dpg.get_value("output_dir") @@ -702,6 +738,8 @@ class App: disc_sec = proj.setdefault("discussion", {}) disc_sec["roles"] = self.disc_roles disc_sec["active"] = self.active_discussion + if dpg.does_item_exist("auto_add_history"): + disc_sec["auto_add"] = dpg.get_value("auto_add_history") def _flush_to_config(self): """Pull global settings into self.config (config.toml).""" @@ -988,13 +1026,30 @@ class App: ai_client.set_custom_system_prompt("\n\n".join(combined_sp)) def do_send(): + auto_add = dpg.get_value("auto_add_history") if dpg.does_item_exist("auto_add_history") else False + if auto_add: + self._queue_history_add("User", user_msg) try: response = ai_client.send(self.last_md, user_msg, base_dir, self.last_file_items) self._update_response(response) self._update_status("done") - except Exception as e: - self._update_response(f"ERROR: {e}") + self._trigger_blink = True + if auto_add: + self._queue_history_add("AI", response) + except ProviderError as e: + resp = e.ui_message() + self._update_response(resp) self._update_status("error") + self._trigger_blink = True + if auto_add: + self._queue_history_add("Vendor API", resp) + except Exception as e: + resp = f"ERROR: {e}" + self._update_response(resp) + self._update_status("error") + self._trigger_blink = True + if auto_add: + self._queue_history_add("System", resp) self.send_thread = threading.Thread(target=do_send, daemon=True) self.send_thread.start() @@ -1385,7 +1440,7 @@ class App: tag="win_projects", pos=(8, 8), width=400, - height=260, + height=340, no_close=True, ): proj_meta = self.project.get("project", {}) @@ -1410,6 +1465,15 @@ class App: ) 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=-40, border=True): pass @@ -1418,35 +1482,11 @@ class App: dpg.add_button(label="New Project", callback=self.cb_new_project) dpg.add_button(label="Save All", callback=self.cb_save_config) - # ---- Config panel ---- - with dpg.window( - label="Config", - tag="win_config", - pos=(8, 276), - width=400, - height=160, - no_close=True, - ): - dpg.add_text("Namespace") - dpg.add_input_text( - tag="namespace", - default_value=self.project.get("output", {}).get("namespace", ""), - width=-1, - ) - 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=-220, - ) - dpg.add_button(label="Browse Output Dir", callback=self.cb_browse_output) - # ---- Files panel ---- with dpg.window( label="Files", tag="win_files", - pos=(8, 444), + pos=(8, 356), width=400, height=400, no_close=True, @@ -1520,6 +1560,11 @@ class App: dpg.add_button(label="+All", callback=self.cb_disc_expand_all) 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): @@ -1695,13 +1740,63 @@ class App: self._fetch_models(self.current_provider) while dpg.is_dearpygui_running(): - # Show any pending confirmation dialog on the main thread + # Show any pending confirmation dialog on the main thread safely with self._pending_dialog_lock: dialog = self._pending_dialog self._pending_dialog = None if dialog is not None: dialog.show() + # Process queued history additions + with self._pending_history_adds_lock: + adds = self._pending_history_adds[:] + self._pending_history_adds.clear() + if adds: + for item in adds: + if item["role"] not in self.disc_roles: + self.disc_roles.append(item["role"]) + self._rebuild_disc_roles_list() + self.disc_entries.append(item) + self._rebuild_disc_list() + if dpg.does_item_exist("disc_scroll"): + # Force scroll to bottom using a very large number + dpg.set_y_scroll("disc_scroll", 99999) + + # Handle retro arcade blinking effect + if self._trigger_blink: + 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 self._is_blinking: + elapsed = time.time() - self._blink_start_time + if elapsed > 1.5: + self._is_blinking = False + if dpg.does_item_exist("response_blink_theme"): + try: + dpg.bind_item_theme("ai_response", 0) + except Exception: + pass + else: + # Square-wave style retro blink (4 times per second) + val = math.sin(elapsed * 8 * math.pi) + alpha = 120 if val > 0 else 0 + + if not dpg.does_item_exist("response_blink_theme"): + with dpg.theme(tag="response_blink_theme"): + with dpg.theme_component(dpg.mvInputText): + dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (0, 255, 0, alpha), tag="response_blink_color") + else: + dpg.set_value("response_blink_color", [0, 255, 0, alpha]) + + if dpg.does_item_exist("ai_response"): + try: + dpg.bind_item_theme("ai_response", "response_blink_theme") + except Exception: + pass + # Flush any comms entries queued from background threads self._flush_pending_comms() @@ -1725,4 +1820,3 @@ def main(): if __name__ == "__main__": main() - \ No newline at end of file diff --git a/manual_slop.toml b/manual_slop.toml index 3a2e27b..b2f93de 100644 --- a/manual_slop.toml +++ b/manual_slop.toml @@ -2,7 +2,7 @@ name = "manual_slop" git_dir = "C:/projects/manual_slop" system_prompt = "Make sure to update MainContext.md every time.\nMake destructive modifications to the project, ITS OK, I HAVE GIT HISTORY TO MANAGE THE PROJECTS." -main_context = "" +main_context = "C:/projects/manual_slop/MainContext.md" [output] namespace = "manual_slop" @@ -39,6 +39,7 @@ roles = [ "System", ] active = "Misc UX improvements" +auto_add = true [discussion.discussions.main] git_commit = "3b2da7873f839bf062c06c7539fadd9df7a3e2b9" @@ -111,5 +112,8 @@ history = [ [discussion.discussions."Misc UX improvements"] git_commit = "" -last_updated = "2026-02-21T22:47:28" -history = [] +last_updated = "2026-02-21T23:08:11" +history = [ + "@2026-02-21T23:03:51\nUser:\nMake the namespace for the generated markdown file the projet name instead of that specific namespace in the config panel.\nThe ouptut dir can be in the project panel spec (that way we can just remove config panel entirely).", + "@2026-02-21T23:04:55\nAI:\nI have applied the changes to streamline the project settings and remove the old Config panel completely:\n\n1. **GUI Layout Changes (`gui.py`)**:\n - The `Config` panel has been completely removed.\n - The **Output Dir** field has been moved into the **Projects** panel (right above the Project Files list).\n - The **Auto-add history** checkbox has been moved to the top of the **Discussion History** panel (directly under the entry toolbar buttons).\n - The **Files** panel was shifted upward to reclaim the space left behind by the old Config panel.\n\n2. **Namespace Management (`aggregate.py` & `project_manager.py`)**:\n - The explicitly defined `namespace` field is gone.\n - Instead, `aggregate.py` now directly infers the output markdown file namespace from the active project's `name` property. (e.g., saving under the `manual_slop` project will just prefix the markdown with `manual_slop_001.md`).\n - `project_manager.py` was updated to no longer scaffold out `namespace` strings into new `.toml` files, and `flat_config` now injects the core `project` dictionary segment so the generators can access the internal name.\n\n3. **`MainContext.md` Updated**:\n - I have updated the markdown file with the latest layout structures and explained that `namespace` is no longer a configurable setting but dynamically determined by the project profile.\n\nYou'll need to restart the application for the GUI layout and DearPyGUI state changes to take effect! Let me know how it looks.", +] diff --git a/project_manager.py b/project_manager.py index 1d794c7..8728a34 100644 --- a/project_manager.py +++ b/project_manager.py @@ -91,7 +91,7 @@ def default_discussion() -> dict: def default_project(name: str = "unnamed") -> dict: return { "project": {"name": name, "git_dir": "", "system_prompt": "", "main_context": ""}, - "output": {"namespace": name, "output_dir": "./md_gen"}, + "output": {"output_dir": "./md_gen"}, "files": {"base_dir": ".", "paths": []}, "screenshots": {"base_dir": ".", "paths": []}, "discussion": { @@ -139,6 +139,7 @@ def flat_config(proj: dict, disc_name: str | None = None) -> dict: name = disc_name or disc_sec.get("active", "main") disc_data = disc_sec.get("discussions", {}).get(name, {}) return { + "project": proj.get("project", {}), "output": proj.get("output", {}), "files": proj.get("files", {}), "screenshots": proj.get("screenshots", {}),