latest ux and readme update

This commit is contained in:
2026-02-21 23:38:24 -05:00
parent 5f9b270841
commit 3e94c9580e
8 changed files with 158 additions and 32 deletions

154
gui.py
View File

@@ -67,27 +67,38 @@ _SUBHDR_COLOR = (220, 200, 120) # sub-section header
def _show_text_viewer(title: str, text: str):
def _show_text_viewer(title: str, text: str, app_instance=None):
if dpg.does_item_exist("win_text_viewer"):
dpg.set_value("text_viewer_content", text if text is not None else "")
wrap = app_instance.project.get("project", {}).get("word_wrap", False) if app_instance else False
dpg.configure_item("win_text_viewer", label=f"Text Viewer - {title}", show=True)
if dpg.does_item_exist("text_viewer_content"):
dpg.set_value("text_viewer_content", text if text is not None else "")
dpg.configure_item("text_viewer_content", show=not wrap)
if dpg.does_item_exist("text_viewer_wrap_container"):
dpg.set_value("text_viewer_wrap", text if text is not None else "")
dpg.configure_item("text_viewer_wrap_container", show=wrap)
dpg.focus_item("win_text_viewer")
def _add_text_field(parent: str, label: str, value: str):
"""Render a labelled text value; long values get a scrollable box."""
wrap = dpg.get_value("project_word_wrap") if dpg.does_item_exist("project_word_wrap") else False
with dpg.group(horizontal=False, parent=parent):
with dpg.group(horizontal=True):
dpg.add_text(f"{label}:", color=_LABEL_COLOR)
dpg.add_button(label="[+]", callback=lambda s, a, u: _show_text_viewer(label, u), user_data=value)
dpg.add_button(label="[+]", callback=lambda s, a, u: _show_text_viewer(label, u, app_instance=self), user_data=value)
if len(value) > COMMS_CLAMP_CHARS:
dpg.add_input_text(
default_value=value,
multiline=True,
readonly=True,
width=-1,
height=80,
)
if wrap:
with dpg.child_window(height=80, border=True):
dpg.add_text(value, wrap=0, color=_VALUE_COLOR)
else:
dpg.add_input_text(
default_value=value,
multiline=True,
readonly=True,
width=-1,
height=80,
)
else:
dpg.add_text(value if value else "(empty)", wrap=0, color=_VALUE_COLOR)
@@ -283,7 +294,7 @@ class ConfirmDialog:
dpg.add_button(
label="[+ Maximize]",
user_data=f"{self._tag}_script",
callback=lambda s, a, u: _show_text_viewer("Confirm Script", dpg.get_value(u))
callback=lambda s, a, u: _show_text_viewer("Confirm Script", dpg.get_value(u, app_instance=self))
)
dpg.add_input_text(
tag=f"{self._tag}_script",
@@ -519,6 +530,9 @@ class App:
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))
if dpg.does_item_exist("project_word_wrap"):
dpg.set_value("project_word_wrap", proj.get("project", {}).get("word_wrap", True))
self.cb_word_wrap_toggled(app_data=proj.get("project", {}).get("word_wrap", True))
def _save_active_project(self):
"""Write self.project to the active project .toml file."""
@@ -708,13 +722,20 @@ class App:
if dpg.does_item_exist("last_script_text"):
dpg.set_value("last_script_text", script)
if dpg.does_item_exist("last_script_text_wrap"):
dpg.set_value("last_script_text_wrap", script)
if dpg.does_item_exist("last_script_output"):
dpg.set_value("last_script_output", result)
if dpg.does_item_exist("last_script_output_wrap"):
dpg.set_value("last_script_output_wrap", result)
self._trigger_script_blink = True
def _rebuild_tool_log(self):
if not dpg.does_item_exist("tool_log_scroll"):
return
wrap = dpg.get_value("project_word_wrap") if dpg.does_item_exist("project_word_wrap") else False
dpg.delete_item("tool_log_scroll", children_only=True)
for i, (script, result) in enumerate(self._tool_log, 1):
with dpg.group(parent="tool_log_scroll"):
@@ -724,20 +745,24 @@ class App:
dpg.add_button(
label="[+ Script]",
user_data=script,
callback=lambda s, a, u: _show_text_viewer(f"Call Script", u)
callback=lambda s, a, u: _show_text_viewer(f"Call Script", u, app_instance=self)
)
dpg.add_button(
label="[+ Output]",
user_data=result,
callback=lambda s, a, u: _show_text_viewer(f"Call Output", u)
callback=lambda s, a, u: _show_text_viewer(f"Call Output", u, app_instance=self)
)
if wrap:
with dpg.child_window(height=72, border=True):
dpg.add_text(result, wrap=0)
else:
dpg.add_input_text(
default_value=result,
multiline=True,
readonly=True,
width=-1,
height=72,
)
dpg.add_input_text(
default_value=result,
multiline=True,
readonly=True,
width=-1,
height=72,
)
dpg.add_separator()
# ---------------------------------------------------------------- helpers
@@ -771,6 +796,8 @@ class App:
proj["project"]["system_prompt"] = dpg.get_value("project_system_prompt")
if dpg.does_item_exist("project_main_context"):
proj["project"]["main_context"] = dpg.get_value("project_main_context")
if dpg.does_item_exist("project_word_wrap"):
proj["project"]["word_wrap"] = dpg.get_value("project_word_wrap")
# Discussion
self._flush_disc_entries_to_project()
@@ -811,6 +838,8 @@ class App:
self.ai_response = text
if dpg.does_item_exist("ai_response"):
dpg.set_value("ai_response", text)
if dpg.does_item_exist("ai_response_wrap"):
dpg.set_value("ai_response_wrap", text)
def _rebuild_files_list(self):
if not dpg.does_item_exist("files_scroll"):
@@ -959,6 +988,32 @@ class App:
# ---------------------------------------------------------------- callbacks
def cb_word_wrap_toggled(self, sender=None, app_data=None):
# This function is now also called by _refresh_project_widgets to set initial state
if app_data is None:
wrap = dpg.get_value("project_word_wrap") if dpg.does_item_exist("project_word_wrap") else False
else:
wrap = app_data
# Persist the setting
self.project.setdefault("project", {})["word_wrap"] = wrap
# Toggle visibility of persistent wrapped/unwrapped container pairs
persistent_panels = [
"ai_response", "last_script_text", "last_script_output", "text_viewer_content"
]
for name in persistent_panels:
no_wrap_widget = name
wrap_container = f"{name}_wrap_container"
if dpg.does_item_exist(no_wrap_widget):
dpg.configure_item(no_wrap_widget, show=not wrap)
if dpg.does_item_exist(wrap_container):
dpg.configure_item(wrap_container, show=wrap)
# Re-render UI components with dynamic content that needs to change widget type
self._rebuild_comms_log()
self._rebuild_tool_log()
def cb_browse_output(self):
root = hide_tk_root()
d = filedialog.askdirectory(title="Select Output Dir")
@@ -1325,12 +1380,23 @@ class App:
width=36,
callback=self._make_disc_insert_cb(i),
)
dpg.add_button(
label="[+ Max]",
user_data=f"disc_content_{{i}}",
callback=lambda s, a, u, idx=i: _show_text_viewer(f"Entry #{{idx+1}}", dpg.get_value(u, app_instance=self) if dpg.does_item_exist(u) else "", app_instance=self)
)
dpg.add_button(
label="Del",
width=36,
callback=self._make_disc_insert_cb(i),
)
dpg.add_button(
label="Del",
width=36,
callback=self._make_disc_remove_cb(i),
)
dpg.add_text(preview, color=(160, 160, 150))
else:
with dpg.group(tag=f"disc_body_{i}", show=not collapsed):
dpg.add_input_text(
tag=f"disc_content_{i}",
@@ -1479,7 +1545,7 @@ class App:
tag="win_projects",
pos=(8, 8),
width=400,
height=340,
height=380,
no_close=True,
):
proj_meta = self.project.get("project", {})
@@ -1514,20 +1580,26 @@ class App:
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):
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
)
# ---- Files panel ----
with dpg.window(
label="Files",
tag="win_files",
pos=(8, 356),
pos=(8, 396),
width=400,
height=400,
height=360,
no_close=True,
):
dpg.add_text("Base Dir")
@@ -1687,6 +1759,8 @@ class App:
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)
@@ -1776,7 +1850,7 @@ class App:
dpg.add_button(
label="[+ Maximize]",
user_data="last_script_text",
callback=lambda s, a, u: _show_text_viewer("Last Script", dpg.get_value(u))
callback=lambda s, a, u: _show_text_viewer("Last Script", dpg.get_value(u, app_instance=self))
)
dpg.add_input_text(
tag="last_script_text",
@@ -1785,13 +1859,15 @@ class App:
width=-1,
height=200,
)
with dpg.child_window(tag="last_script_text_wrap_container", width=-1, height=200, border=True, show=False):
dpg.add_text("", tag="last_script_text_wrap", wrap=0)
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_text("Output:")
dpg.add_button(
label="[+ Maximize]",
user_data="last_script_output",
callback=lambda s, a, u: _show_text_viewer("Last Output", dpg.get_value(u))
callback=lambda s, a, u: _show_text_viewer("Last Output", dpg.get_value(u, app_instance=self))
)
dpg.add_input_text(
tag="last_script_output",
@@ -1800,6 +1876,8 @@ class App:
width=-1,
height=-1,
)
with dpg.child_window(tag="last_script_output_wrap_container", width=-1, height=-1, border=True, show=False):
dpg.add_text("", tag="last_script_output_wrap", wrap=0)
# ---- Global Text Viewer Popup ----
with dpg.window(
@@ -1818,6 +1896,8 @@ class App:
width=-1,
height=-1,
)
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)
def run(self):
dpg.create_context()
@@ -1876,6 +1956,10 @@ class App:
try:
dpg.bind_item_theme("last_script_output", 0)
dpg.bind_item_theme("last_script_text", 0)
if dpg.does_item_exist("last_script_output_wrap_container"):
dpg.bind_item_theme("last_script_output_wrap_container", 0)
if dpg.does_item_exist("last_script_text_wrap_container"):
dpg.bind_item_theme("last_script_text_wrap_container", 0)
except Exception:
pass
else:
@@ -1884,15 +1968,22 @@ class App:
if not dpg.does_item_exist("script_blink_theme"):
with dpg.theme(tag="script_blink_theme"):
with dpg.theme_component(dpg.mvInputText):
with dpg.theme_component(dpg.mvAll):
dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (0, 100, 255, alpha), tag="script_blink_color")
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 100, 255, alpha), tag="script_blink_color2")
else:
dpg.set_value("script_blink_color", [0, 100, 255, alpha])
if dpg.does_item_exist("script_blink_color2"):
dpg.set_value("script_blink_color2", [0, 100, 255, alpha])
if dpg.does_item_exist("last_script_output"):
try:
dpg.bind_item_theme("last_script_output", "script_blink_theme")
dpg.bind_item_theme("last_script_text", "script_blink_theme")
if dpg.does_item_exist("last_script_output_wrap_container"):
dpg.bind_item_theme("last_script_output_wrap_container", "script_blink_theme")
if dpg.does_item_exist("last_script_text_wrap_container"):
dpg.bind_item_theme("last_script_text_wrap_container", "script_blink_theme")
except Exception:
pass
@@ -1910,6 +2001,8 @@ class App:
if dpg.does_item_exist("response_blink_theme"):
try:
dpg.bind_item_theme("ai_response", 0)
if dpg.does_item_exist("ai_response_wrap_container"):
dpg.bind_item_theme("ai_response_wrap_container", 0)
except Exception:
pass
else:
@@ -1919,14 +2012,19 @@ class App:
if not dpg.does_item_exist("response_blink_theme"):
with dpg.theme(tag="response_blink_theme"):
with dpg.theme_component(dpg.mvInputText):
with dpg.theme_component(dpg.mvAll):
dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (0, 255, 0, alpha), tag="response_blink_color")
dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 255, 0, alpha), tag="response_blink_color2")
else:
dpg.set_value("response_blink_color", [0, 255, 0, alpha])
if dpg.does_item_exist("response_blink_color2"):
dpg.set_value("response_blink_color2", [0, 255, 0, alpha])
if dpg.does_item_exist("ai_response"):
try:
dpg.bind_item_theme("ai_response", "response_blink_theme")
if dpg.does_item_exist("ai_response_wrap_container"):
dpg.bind_item_theme("ai_response_wrap_container", "response_blink_theme")
except Exception:
pass