feat(ui): Implement walkthrough refinements (Diagnostics, Tabs, Selectable text, Session Loading)

This commit is contained in:
2026-02-23 18:57:02 -05:00
parent ae5dd328e1
commit ebd81586bb
2 changed files with 165 additions and 75 deletions

144
gui.py
View File

@@ -128,7 +128,8 @@ def _add_text_field(parent: str, label: str, value: str):
if len(value) > COMMS_CLAMP_CHARS: if len(value) > COMMS_CLAMP_CHARS:
if wrap: if wrap:
with dpg.child_window(height=80, border=True): 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: else:
dpg.add_input_text( dpg.add_input_text(
default_value=value, default_value=value,
@@ -138,15 +139,15 @@ def _add_text_field(parent: str, label: str, value: str):
height=80, height=80,
) )
else: 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): def _add_kv_row(parent: str, key: str, val, val_color=None):
"""Single key: value row, horizontally laid out.""" """Single key: value row, horizontally laid out."""
vc = val_color or _VALUE_COLOR
with dpg.group(horizontal=True, parent=parent): with dpg.group(horizontal=True, parent=parent):
dpg.add_text(f"{key}:", color=_LABEL_COLOR) 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): def _render_usage(parent: str, usage: dict):
@@ -451,6 +452,7 @@ class App:
"AI Settings Hub": "win_ai_settings_hub", "AI Settings Hub": "win_ai_settings_hub",
"Discussion Hub": "win_discussion_hub", "Discussion Hub": "win_discussion_hub",
"Operations Hub": "win_operations_hub", "Operations Hub": "win_operations_hub",
"Diagnostics": "win_diagnostics",
"Theme": "win_theme", "Theme": "win_theme",
"Last Script Output": "win_script_output", "Last Script Output": "win_script_output",
"Text Viewer": "win_text_viewer", "Text Viewer": "win_text_viewer",
@@ -489,6 +491,8 @@ class App:
self._is_script_blinking = False self._is_script_blinking = False
self._script_blink_start_time = 0.0 self._script_blink_start_time = 0.0
self.is_viewing_prior_session = False
# Subscribe to API lifecycle events # Subscribe to API lifecycle events
ai_client.events.on("request_start", self._on_api_event) ai_client.events.on("request_start", self._on_api_event)
ai_client.events.on("response_received", self._on_api_event) ai_client.events.on("response_received", self._on_api_event)
@@ -863,7 +867,7 @@ class App:
# Update Diagnostics panel (throttled for smoothness) # Update Diagnostics panel (throttled for smoothness)
if now - self._last_perf_update_time > 0.5: if now - self._last_perf_update_time > 0.5:
self._last_perf_update_time = now 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() metrics = self.perf_monitor.get_metrics()
# Update history # Update history
@@ -1310,6 +1314,65 @@ class App:
except Exception as e: except Exception as e:
self._update_status(f"error: {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): def cb_reset_session(self):
ai_client.reset_session() ai_client.reset_session()
ai_client.clear_comms_log() ai_client.clear_comms_log()
@@ -1586,8 +1649,14 @@ class App:
# ---- disc entry list ---- # ---- disc entry list ----
def _render_disc_entry(self, i: int, entry: dict): def _render_disc_entry(self, i: int, entry: dict):
collapsed = entry.get("collapsed", False) # Default to collapsed and read-mode if not specified
read_mode = entry.get("read_mode", False) 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", "") ts_str = entry.get("ts", "")
preview = entry["content"].replace("\n", " ")[:60] preview = entry["content"].replace("\n", " ")[:60]
@@ -1602,6 +1671,11 @@ class App:
width=24, width=24,
callback=self._make_disc_toggle_cb(i), 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( dpg.add_combo(
tag=f"disc_role_{i}", tag=f"disc_role_{i}",
items=self.disc_roles, items=self.disc_roles,
@@ -1623,11 +1697,6 @@ class App:
width=36, width=36,
callback=self._make_disc_insert_cb(i), 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( dpg.add_button(
label="Del", label="Del",
width=36, width=36,
@@ -1637,8 +1706,14 @@ class App:
with dpg.group(tag=f"disc_body_{i}", show=not collapsed): with dpg.group(tag=f"disc_body_{i}", show=not collapsed):
if read_mode: if read_mode:
with dpg.child_window(height=150, border=True): # Use a read-only input_text instead of dpg.add_text to allow selection
dpg.add_text(entry["content"], wrap=0, color=(200, 200, 200)) dpg.add_input_text(
default_value=entry["content"],
multiline=True,
readonly=True,
width=-1,
height=150,
)
else: else:
dpg.add_input_text( dpg.add_input_text(
tag=f"disc_content_{i}", tag=f"disc_content_{i}",
@@ -1972,6 +2047,11 @@ class App:
no_close=False, no_close=False,
no_collapse=True, 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 # History at Top
with dpg.child_window(tag="disc_history_section", height=-400, border=True): with dpg.child_window(tag="disc_history_section", height=-400, border=True):
# Discussion selector section # Discussion selector section
@@ -2008,17 +2088,16 @@ class App:
with dpg.child_window(tag="disc_scroll", height=-1, border=False): with dpg.child_window(tag="disc_scroll", height=-1, border=False):
pass pass
# Message Composer in Middle dpg.add_separator()
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)
# Interaction Tabs at Bottom
with dpg.tab_bar():
with dpg.tab(label="Message"):
dpg.add_input_text( dpg.add_input_text(
tag="ai_input", tag="ai_input",
multiline=True, multiline=True,
width=-1, width=-1,
height=120, height=200,
) )
with dpg.group(horizontal=True): with dpg.group(horizontal=True):
dpg.add_button(label="Gen + Send", callback=self.cb_generate_send) dpg.add_button(label="Gen + Send", callback=self.cb_generate_send)
@@ -2026,10 +2105,7 @@ class App:
dpg.add_button(label="Reset", callback=self.cb_reset_session) dpg.add_button(label="Reset", callback=self.cb_reset_session)
dpg.add_button(label="-> History", callback=self.cb_append_message_to_history) dpg.add_button(label="-> History", callback=self.cb_append_message_to_history)
dpg.add_separator() with dpg.tab(label="AI Response"):
# AI Response at Bottom
dpg.add_text("AI Response", color=_SUBHDR_COLOR)
dpg.add_input_text( dpg.add_input_text(
tag="ai_response", tag="ai_response",
multiline=True, multiline=True,
@@ -2063,6 +2139,10 @@ class App:
dpg.add_text("Status: idle", tag="ai_status", color=(200, 220, 160)) dpg.add_text("Status: idle", tag="ai_status", color=(200, 220, 160))
dpg.add_spacer(width=16) dpg.add_spacer(width=16)
dpg.add_button(label="Clear", callback=self.cb_clear_comms) 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_text("Tokens: 0 (In: 0 Out: 0)", tag="ai_token_usage", color=(180, 255, 180))
dpg.add_separator() dpg.add_separator()
with dpg.child_window(tag="comms_scroll", height=-1, border=False, horizontal_scrollbar=True): with dpg.child_window(tag="comms_scroll", height=-1, border=False, horizontal_scrollbar=True):
@@ -2076,7 +2156,16 @@ class App:
with dpg.child_window(tag="tool_log_scroll", height=-1, border=False): with dpg.child_window(tag="tool_log_scroll", height=-1, border=False):
pass pass
with dpg.tab(label="Diagnostics"): 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") dpg.add_text("Performance Telemetry")
with dpg.table(header_row=False, borders_innerH=True, borders_outerH=True, borders_innerV=True, borders_outerV=True): 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()
@@ -2095,13 +2184,13 @@ class App:
dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80)) dpg.add_text("0.0ms", tag="perf_lag_text", color=(255, 180, 80))
dpg.add_spacer(height=4) 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(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") 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"): 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.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.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(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") 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"): 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.add_line_series(list(range(100)), self.perf_history["cpu"], label="cpu usage", tag="perf_cpu_plot")
@@ -2127,6 +2216,7 @@ class App:
self._build_ai_settings_hub() self._build_ai_settings_hub()
self._build_discussion_hub() self._build_discussion_hub()
self._build_operations_hub() self._build_operations_hub()
self._build_diagnostics_window()
self._build_theme_window() self._build_theme_window()

View File

@@ -50,7 +50,7 @@ def test_old_windows_removed_from_window_info(app_instance_simple):
"win_projects", "win_files", "win_screenshots", "win_projects", "win_files", "win_screenshots",
"win_provider", "win_system_prompts", "win_provider", "win_system_prompts",
"win_discussion", "win_message", "win_response", "win_discussion", "win_message", "win_response",
"win_comms", "win_tool_log", "win_diagnostics" "win_comms", "win_tool_log"
] ]
for tag in old_tags: for tag in old_tags: