diff --git a/cheddahbot/agent.py b/cheddahbot/agent.py index 76d2c4e..a0f4173 100644 --- a/cheddahbot/agent.py +++ b/cheddahbot/agent.py @@ -114,6 +114,9 @@ class Agent: """Process user input and yield streaming response text.""" conv_id = self.ensure_conversation() + # Auto-title early so it's set even if the generator is closed mid-stream + self._maybe_set_title(conv_id, user_input) + # Store user message self.db.add_message(conv_id, "user", user_input) @@ -285,9 +288,6 @@ class Agent: else: yield "\n(Reached maximum tool iterations)" - # Auto-title the conversation from the first user message - self._maybe_set_title(conv_id, user_input) - # Check if memory flush is needed if self._memory: msg_count = self.db.count_messages(conv_id) @@ -295,9 +295,10 @@ class Agent: self._memory.auto_flush(conv_id) def _maybe_set_title(self, conv_id: str, user_input: str): - """Set conversation title from first user message if still untitled.""" + """Set conversation title from first user message if still 'New Chat'.""" try: - if self.db.count_messages(conv_id) > 3: + current_title = self.db.get_conversation_title(conv_id) + if current_title and current_title != "New Chat": return title = user_input.split("\n", 1)[0].strip() if len(title) > 50: diff --git a/cheddahbot/ui.py b/cheddahbot/ui.py index 955fb30..07a3c83 100644 --- a/cheddahbot/ui.py +++ b/cheddahbot/ui.py @@ -138,6 +138,7 @@ def create_ui( config: Config, llm: LLMAdapter, notification_bus: NotificationBus | None = None, + scheduler=None, ) -> gr.Blocks: """Build and return the Gradio app.""" @@ -220,6 +221,16 @@ def create_ui( elem_classes=["contain", "notification-banner"], ) + # -- System Loop Monitoring -- + with gr.Row(elem_classes=["contain"]): + loop_status = gr.Markdown( + value="*Recent System events* | System Loop: waiting for first run...", + ) + force_pulse_btn = gr.Button( + "Force Pulse", variant="secondary", size="sm", + scale=0, min_width=110, + ) + with gr.Row(elem_classes=["contain"]): model_dropdown = gr.Dropdown( choices=model_choices, @@ -361,6 +372,7 @@ def create_ui( try: response_text = "" assistant_added = False + title_refreshed = False for chunk in agent.respond(text, files=processed_files): response_text += chunk @@ -372,7 +384,16 @@ def create_ui( assistant_added = True else: chat_history[-1] = {"role": "assistant", "content": response_text} - yield chat_history, gr.update(value=None), gr.update() + + # Refresh conv list on first chunk so sidebar shows + # the updated title immediately (title is set at the + # start of agent.respond before streaming begins). + if not title_refreshed: + title_refreshed = True + convs = _refresh_conv_list(agent_name) + yield chat_history, gr.update(value=None), convs + else: + yield chat_history, gr.update(value=None), gr.update() # If no response came through, show a fallback if not response_text: @@ -455,8 +476,53 @@ def create_ui( banner = "\n\n".join(lines) return gr.update(value=banner, visible=True) + def poll_loop_status(): + """Poll scheduler loop timestamps and format for display.""" + if not scheduler: + return gr.update() + from datetime import UTC + from datetime import datetime as _dt + + ts = scheduler.get_loop_timestamps() + parts = [] + for name, iso_ts in ts.items(): + if iso_ts: + try: + dt = _dt.fromisoformat(iso_ts) + delta = _dt.now(UTC) - dt + secs = int(delta.total_seconds()) + if secs < 60: + ago = f"{secs}s ago" + elif secs < 3600: + ago = f"{secs // 60}m ago" + else: + ago = f"{secs // 3600}h {(secs % 3600) // 60}m ago" + parts.append(f"{name}: {ago}") + except Exception: + parts.append(f"{name}: {iso_ts}") + if parts: + return gr.update( + value=f"*Recent System events* | System Loop: {' | '.join(parts)}" + ) + return gr.update( + value="*Recent System events* | System Loop: waiting for first run..." + ) + + def on_force_pulse(): + if not scheduler: + return gr.update( + value="*Recent System events* | System Loop: scheduler not available" + ) + scheduler.force_heartbeat() + scheduler.force_poll() + return gr.update( + value="*Recent System events* | System Loop: **Force pulse sent!**" + ) + # -- Wire events -- + force_pulse_btn.click(on_force_pulse, None, [loop_status]) + model_dropdown.change(on_model_change, [model_dropdown], None) refresh_btn.click(on_refresh_models, None, [model_dropdown]) @@ -495,4 +561,9 @@ def create_ui( timer = gr.Timer(10) timer.tick(poll_notifications, None, [notification_display]) + # System loop status polling timer (every 30 seconds) + if scheduler: + loop_timer = gr.Timer(30) + loop_timer.tick(poll_loop_status, None, [loop_status]) + return app diff --git a/dashboard/index.html b/dashboard/index.html index 8e705f4..214a90f 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -270,6 +270,15 @@
+Loading loop status...
+Recent system notifications and events
Loading loop status...
+Loading...
Scheduler not available.
'; + return; + } + + let html = ''; + for (const [key, ts] of Object.entries(data.loops)) { + const label = LOOP_LABELS[key] || key; + const { text, status } = formatLoopAgo(ts); + html += `