Wire NotificationBus into main and Gradio UI

Create NotificationBus in __main__.py and inject into scheduler and UI.
Gradio subscribes as the "gradio" listener with a 10-second polling
timer that displays notifications in a banner above the chatbot.
ClickUp status shown in the header bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cora-start
PeninsulaInd 2026-02-15 22:29:33 -06:00
parent a67e714045
commit 7864ca2461
2 changed files with 54 additions and 4 deletions

View File

@ -58,17 +58,26 @@ def main():
except Exception as e:
log.warning("Tool system not available: %s", e)
# Notification bus (UI-agnostic)
notification_bus = None
try:
from .notifications import NotificationBus
log.info("Initializing notification bus...")
notification_bus = NotificationBus(db)
except Exception as e:
log.warning("Notification bus not available: %s", e)
# Phase 3+: Scheduler
try:
from .scheduler import Scheduler
log.info("Starting scheduler...")
scheduler = Scheduler(config, db, agent)
scheduler = Scheduler(config, db, agent, notification_bus=notification_bus)
scheduler.start()
except Exception as e:
log.warning("Scheduler not available: %s", e)
log.info("Launching Gradio UI on %s:%s...", config.host, config.port)
app, css = create_ui(agent, config, llm)
app, css = create_ui(agent, config, llm, notification_bus=notification_bus)
app.launch(
server_name=config.host,
server_port=config.port,

View File

@ -13,16 +13,26 @@ if TYPE_CHECKING:
from .agent import Agent
from .config import Config
from .llm import LLMAdapter
from .notifications import NotificationBus
log = logging.getLogger(__name__)
_CSS = """
.contain { max-width: 900px; margin: auto; }
footer { display: none !important; }
.notification-banner {
background: #1a1a2e;
border: 1px solid #16213e;
border-radius: 8px;
padding: 10px 16px;
margin-bottom: 8px;
font-size: 0.9em;
}
"""
def create_ui(agent: Agent, config: Config, llm: LLMAdapter) -> gr.Blocks:
def create_ui(agent: Agent, config: Config, llm: LLMAdapter,
notification_bus: NotificationBus | None = None) -> gr.Blocks:
"""Build and return the Gradio app."""
available_models = llm.list_chat_models()
@ -30,15 +40,24 @@ def create_ui(agent: Agent, config: Config, llm: LLMAdapter) -> gr.Blocks:
current_model = llm.current_model
exec_status = "available" if llm.is_execution_brain_available() else "unavailable"
clickup_status = "enabled" if config.clickup.enabled else "disabled"
with gr.Blocks(title="CheddahBot") as app:
gr.Markdown("# CheddahBot", elem_classes=["contain"])
gr.Markdown(
f"*Chat Brain:* `{current_model}` &nbsp;|&nbsp; "
f"*Execution Brain (Claude Code CLI):* `{exec_status}`",
f"*Execution Brain (Claude Code CLI):* `{exec_status}` &nbsp;|&nbsp; "
f"*ClickUp:* `{clickup_status}`",
elem_classes=["contain"],
)
# -- Notification banner --
notification_display = gr.Markdown(
value="",
visible=False,
elem_classes=["contain", "notification-banner"],
)
with gr.Row(elem_classes=["contain"]):
model_dropdown = gr.Dropdown(
choices=model_choices,
@ -191,6 +210,22 @@ def create_ui(agent: Agent, config: Config, llm: LLMAdapter) -> gr.Blocks:
except Exception as e:
return None, f"Voice chat error: {e}"
def poll_notifications():
"""Poll the notification bus for pending messages."""
if not notification_bus:
return gr.update(value="", visible=False)
messages = notification_bus.get_pending("gradio")
if not messages:
return gr.update() # No change
# Format notifications as markdown
lines = []
for msg in messages[-5:]: # Show last 5 notifications max
lines.append(f"**Notification:** {msg}")
banner = "\n\n".join(lines)
return gr.update(value=banner, visible=True)
# -- Wire events --
model_dropdown.change(on_model_change, [model_dropdown], None)
@ -209,6 +244,12 @@ def create_ui(agent: Agent, config: Config, llm: LLMAdapter) -> gr.Blocks:
[voice_output, voice_status],
)
# Notification polling timer (every 10 seconds)
if notification_bus:
notification_bus.subscribe("gradio", lambda msg, cat: None) # Register listener
timer = gr.Timer(10)
timer.tick(poll_notifications, None, [notification_display])
# Load conversation list on app start
app.load(_load_conversations, None, [conv_list])