This commit is contained in:
2026-02-21 15:35:44 -05:00
parent 308070e885
commit 105a80bbe7
5 changed files with 326 additions and 22 deletions

171
gui.py
View File

@@ -1,4 +1,5 @@
import dearpygui.dearpygui as dpg
# gui.py
import dearpygui.dearpygui as dpg
import tomllib
import tomli_w
import threading
@@ -12,6 +13,9 @@ import shell_runner
CONFIG_PATH = Path("config.toml")
PROVIDERS = ["gemini", "anthropic"]
# Max chars shown inline for a heavy comms field before clamping to a scrollable box
COMMS_CLAMP_CHARS = 300
def load_config() -> dict:
with open(CONFIG_PATH, "rb") as f:
@@ -30,6 +34,76 @@ def hide_tk_root() -> Tk:
return root
# ------------------------------------------------------------------ comms rendering helpers
# Direction -> colour
_DIR_COLORS = {
"OUT": (100, 200, 255), # blue-ish
"IN": (140, 255, 160), # green-ish
}
# Kind -> colour
_KIND_COLORS = {
"request": (255, 220, 100),
"response": (180, 255, 180),
"tool_call": (255, 180, 80),
"tool_result": (180, 220, 255),
"tool_result_send": (200, 180, 255),
}
_HEAVY_KEYS = {"message", "text", "script", "output", "content"}
def _add_comms_field(parent: str, label: str, value: str, heavy: bool):
"""Add a labelled field inside parent. Heavy fields get a clamped input_text box."""
with dpg.group(horizontal=False, parent=parent):
dpg.add_text(f"{label}:", color=(200, 200, 200))
if heavy and len(value) > COMMS_CLAMP_CHARS:
# Show clamped scrollable box
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=460)
def _render_comms_entry(parent: str, entry: dict, idx: int):
direction = entry["direction"]
kind = entry["kind"]
ts = entry["ts"]
provider = entry["provider"]
model = entry["model"]
payload = entry["payload"]
dir_color = _DIR_COLORS.get(direction, (220, 220, 220))
kind_color = _KIND_COLORS.get(kind, (220, 220, 220))
with dpg.group(horizontal=False, parent=parent):
# Header row
with dpg.group(horizontal=True):
dpg.add_text(f"#{idx}", color=(160, 160, 160))
dpg.add_text(ts, color=(160, 160, 160))
dpg.add_text(direction, color=dir_color)
dpg.add_text(kind, color=kind_color)
dpg.add_text(f"{provider}/{model}", color=(180, 180, 180))
# Payload fields
for key, val in payload.items():
is_heavy = key in _HEAVY_KEYS
if isinstance(val, (dict, list)):
import json
val_str = json.dumps(val, ensure_ascii=False, indent=2)
else:
val_str = str(val)
_add_comms_field(parent, key, val_str, is_heavy)
dpg.add_separator()
class ConfirmDialog:
"""
Modal confirmation window for a proposed PowerShell script.
@@ -127,8 +201,45 @@ class App:
self._tool_log: list[tuple[str, str]] = []
# Comms log entries queued from background thread for main-thread rendering
self._pending_comms: list[dict] = []
self._pending_comms_lock = threading.Lock()
self._comms_entry_count = 0
ai_client.set_provider(self.current_provider, self.current_model)
ai_client.confirm_and_run_callback = self._confirm_and_run
ai_client.comms_log_callback = self._on_comms_entry
# ---------------------------------------------------------------- comms log
def _on_comms_entry(self, entry: dict):
"""Called from background thread; queue for main thread."""
with self._pending_comms_lock:
self._pending_comms.append(entry)
def _flush_pending_comms(self):
"""Called every frame from the main render loop."""
with self._pending_comms_lock:
entries = self._pending_comms[:]
self._pending_comms.clear()
for entry in entries:
self._comms_entry_count += 1
self._append_comms_entry(entry, self._comms_entry_count)
def _append_comms_entry(self, entry: dict, idx: int):
if not dpg.does_item_exist("comms_scroll"):
return
_render_comms_entry("comms_scroll", entry, idx)
def _rebuild_comms_log(self):
"""Full redraw from ai_client.get_comms_log() — used after clear/reset."""
if not dpg.does_item_exist("comms_scroll"):
return
dpg.delete_item("comms_scroll", children_only=True)
self._comms_entry_count = 0
for entry in ai_client.get_comms_log():
self._comms_entry_count += 1
_render_comms_entry("comms_scroll", entry, self._comms_entry_count)
# ---------------------------------------------------------------- tool execution
@@ -194,7 +305,7 @@ class App:
self.config["discussion"] = {"history": self.history}
self.config["ai"] = {
"provider": self.current_provider,
"model": self.current_model,
"model": self.current_model,
}
def _do_generate(self) -> tuple[str, Path]:
@@ -357,8 +468,15 @@ class App:
def cb_reset_session(self):
ai_client.reset_session()
ai_client.clear_comms_log()
self._tool_log.clear()
self._rebuild_tool_log()
# Clear pending queue and counter, then wipe the comms panel
with self._pending_comms_lock:
self._pending_comms.clear()
self._comms_entry_count = 0
if dpg.does_item_exist("comms_scroll"):
dpg.delete_item("comms_scroll", children_only=True)
self._update_status("session reset")
self._update_response("")
@@ -411,6 +529,14 @@ class App:
self._tool_log.clear()
self._rebuild_tool_log()
def cb_clear_comms(self):
ai_client.clear_comms_log()
with self._pending_comms_lock:
self._pending_comms.clear()
self._comms_entry_count = 0
if dpg.does_item_exist("comms_scroll"):
dpg.delete_item("comms_scroll", children_only=True)
# ---------------------------------------------------------------- build ui
def _build_ui(self):
@@ -519,7 +645,7 @@ class App:
tag="win_provider",
pos=(1232, 8),
width=420,
height=280,
height=260,
no_close=True,
):
dpg.add_text("Provider")
@@ -542,13 +668,11 @@ class App:
num_items=6,
callback=self.cb_model_changed,
)
dpg.add_separator()
dpg.add_text("Status: idle", tag="ai_status")
with dpg.window(
label="Message",
tag="win_message",
pos=(1232, 296),
pos=(1232, 276),
width=420,
height=280,
no_close=True,
@@ -568,7 +692,7 @@ class App:
with dpg.window(
label="Response",
tag="win_response",
pos=(1232, 584),
pos=(1232, 564),
width=420,
height=300,
no_close=True,
@@ -584,7 +708,7 @@ class App:
with dpg.window(
label="Tool Calls",
tag="win_tool_log",
pos=(1232, 892),
pos=(1232, 872),
width=420,
height=300,
no_close=True,
@@ -596,6 +720,34 @@ class App:
with dpg.child_window(tag="tool_log_scroll", height=-1, border=False):
pass
# ---- Comms History panel (new) ----
with dpg.window(
label="Comms History",
tag="win_comms",
pos=(1660, 8),
width=520,
height=1164,
no_close=True,
):
# Status line lives here now
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_separator()
# Colour legend
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
def run(self):
dpg.create_context()
dpg.configure_app(docking=True, docking_space=True, init_file="dpg_layout.ini")
@@ -614,6 +766,9 @@ class App:
if dialog is not None:
dialog.show()
# Flush any comms entries queued from background threads
self._flush_pending_comms()
dpg.render_dearpygui_frame()
dpg.save_init_file("dpg_layout.ini")