mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
feat(kanban-dashboard): per-platform home-channel notification toggles (#19864)
* revert: auto-subscribe gateway chat on tool-driven kanban_create (#19718)
Reverts ff3d2773e2. Teknium reviewed the merged PR and decided this
behavior isn't wanted — tool-driven kanban_create should not mirror
the slash-command path's auto-subscribe. Orchestrators that want
their originating chat notified can call kanban_notify-subscribe
explicitly; we're not going to make it implicit.
* feat(kanban-dashboard): per-platform home-channel notification toggles
Adds a "Notify home channels" section to the task drawer in the kanban
dashboard plugin. Each platform where the user has set a home channel
(/sethome, TELEGRAM_HOME_CHANNEL env var, gateway.platforms.<p>.home_channel
in config.yaml) gets a toggle pill. Toggling on writes a kanban_notify_subs
row keyed to that platform's home (chat_id + thread_id); toggling off
removes it. The existing gateway notifier watcher delivers completed /
blocked / gave_up events without any new plumbing — this is purely a GUI
surface over existing machinery.
Replaces the reverted auto-subscribe behavior from #19718 with an explicit,
per-task, per-platform, user-controlled opt-in. No implicit subscription
on tool-driven kanban_create; no CLI commands; no slash commands. Just a
toggle in the drawer.
Backend (plugins/kanban/dashboard/plugin_api.py):
- GET /api/plugins/kanban/home-channels[?task_id=X]
Returns every platform with a configured home, plus a per-entry
subscribed: bool relative to task_id (false when task_id omitted).
Reads the live GatewayConfig via load_gateway_config() so env-var
overlays stay honored.
- POST /api/plugins/kanban/tasks/:id/home-subscribe/:platform
Idempotent add_notify_sub keyed to the platform's home.
- DELETE /api/plugins/kanban/tasks/:id/home-subscribe/:platform
remove_notify_sub for the same tuple.
- 404 when the platform has no home configured, or task_id doesn't
exist (POST only).
Frontend (plugins/kanban/dashboard/dist/index.js):
- TaskDrawer fetches /home-channels on open, keyed on task_id.
- HomeSubsSection renders nothing when zero platforms have a home (so
users who haven't set one up don't see an empty UI block).
- Optimistic toggle with busy flag + revert-on-failure. One pill per
platform; ✓ prefix and --on class indicate the subscribed state.
CSS (plugins/kanban/dashboard/dist/style.css):
- .hermes-kanban-home-subs flex row + .hermes-kanban-home-sub pill
style + --on subscribed variant (subtle ring-colored background).
Live-tested against a dashboard with TELEGRAM + DISCORD_BOT_TOKEN /
HOME_CHANNEL env vars set: drawer shows both pills, toggling each
flips its visual state AND writes/removes the correct kanban_notify_subs
row (verified via direct DB read).
Tests (tests/plugins/test_kanban_dashboard_plugin.py, 11 new, 53/53
pass total):
- home-channels lists only platforms with a home (slack with a
token but no home is excluded)
- no task_id -> all subscribed=false
- subscribe creates notify_sub row with correct chat/thread/platform
- subscribed=true reflected in subsequent GET
- idempotent re-subscribe
- unknown platform -> 404
- unknown task -> 404
- unsubscribe removes the row
- telegram + discord subscribe/unsubscribe independent
- zero homes -> empty list
This commit is contained in:
parent
3db6b9cc87
commit
1c7c7c3c5f
4 changed files with 423 additions and 0 deletions
|
|
@ -733,6 +733,155 @@ def get_config():
|
|||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Home-channel subscriptions (per-task, per-platform toggles)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Home channels are a first-class gateway concept — each configured platform
|
||||
# can have exactly one (chat_id, thread_id, name) it considers "home". The
|
||||
# dashboard surfaces these as per-task toggles so a user can opt a specific
|
||||
# task into receiving terminal notifications (completed / blocked / gave_up)
|
||||
# at their telegram/discord/slack home, without touching the CLI.
|
||||
#
|
||||
# The wire format mirrors kanban_db.add_notify_sub — (task_id, platform,
|
||||
# chat_id, thread_id) — so toggle-on creates exactly the same row the
|
||||
# `/kanban create` slash command would, and the existing gateway notifier
|
||||
# watcher delivers events without any additional plumbing.
|
||||
|
||||
|
||||
def _configured_home_channels() -> list[dict]:
|
||||
"""Return every platform that has a home_channel set, fully hydrated.
|
||||
|
||||
Reads the live GatewayConfig so env-var overlays (``TELEGRAM_HOME_CHANNEL``
|
||||
etc.) are honored alongside config.yaml. Returns platforms in a stable
|
||||
order and drops platforms without a home.
|
||||
"""
|
||||
try:
|
||||
from gateway.config import load_gateway_config
|
||||
except Exception:
|
||||
return []
|
||||
try:
|
||||
gw_cfg = load_gateway_config()
|
||||
except Exception:
|
||||
return []
|
||||
result: list[dict] = []
|
||||
for platform, pcfg in gw_cfg.platforms.items():
|
||||
if not pcfg or not pcfg.home_channel:
|
||||
continue
|
||||
hc = pcfg.home_channel
|
||||
result.append({
|
||||
"platform": platform.value,
|
||||
"chat_id": hc.chat_id,
|
||||
"thread_id": hc.thread_id or "",
|
||||
"name": hc.name or "Home",
|
||||
})
|
||||
# Stable order for deterministic UI — platform name alphabetical.
|
||||
result.sort(key=lambda r: r["platform"])
|
||||
return result
|
||||
|
||||
|
||||
def _home_sub_matches(sub: dict, home: dict) -> bool:
|
||||
"""True if a notify_subs row corresponds to the given home channel."""
|
||||
return (
|
||||
sub.get("platform") == home["platform"]
|
||||
and str(sub.get("chat_id", "")) == str(home["chat_id"])
|
||||
and str(sub.get("thread_id") or "") == str(home["thread_id"] or "")
|
||||
)
|
||||
|
||||
|
||||
@router.get("/home-channels")
|
||||
def get_home_channels(
|
||||
task_id: Optional[str] = Query(None),
|
||||
board: Optional[str] = Query(None),
|
||||
):
|
||||
"""List every platform with a home channel, plus whether *task_id*
|
||||
(if given) is currently subscribed to that home.
|
||||
|
||||
When ``task_id`` is omitted, every entry's ``subscribed`` is ``false``
|
||||
— useful for the "no task selected" state of the UI.
|
||||
"""
|
||||
homes = _configured_home_channels()
|
||||
subscribed_homes: set[tuple[str, str, str]] = set()
|
||||
if task_id:
|
||||
board = _resolve_board(board)
|
||||
conn = _conn(board=board)
|
||||
try:
|
||||
subs = kanban_db.list_notify_subs(conn, task_id)
|
||||
finally:
|
||||
conn.close()
|
||||
for sub in subs:
|
||||
key = (
|
||||
str(sub.get("platform") or ""),
|
||||
str(sub.get("chat_id") or ""),
|
||||
str(sub.get("thread_id") or ""),
|
||||
)
|
||||
subscribed_homes.add(key)
|
||||
result = []
|
||||
for home in homes:
|
||||
key = (home["platform"], home["chat_id"], home["thread_id"])
|
||||
result.append({**home, "subscribed": key in subscribed_homes})
|
||||
return {"home_channels": result}
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/home-subscribe/{platform}")
|
||||
def subscribe_home(task_id: str, platform: str, board: Optional[str] = Query(None)):
|
||||
"""Subscribe *task_id* to notifications routed to *platform*'s home channel.
|
||||
|
||||
Idempotent — re-subscribing is a no-op at the DB layer. 404 if the
|
||||
platform has no home channel configured. 404 if the task doesn't exist.
|
||||
"""
|
||||
homes = _configured_home_channels()
|
||||
home = next((h for h in homes if h["platform"] == platform), None)
|
||||
if not home:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No home channel configured for platform {platform!r}. "
|
||||
f"Set one from the messenger via /sethome, or configure "
|
||||
f"gateway.platforms.{platform}.home_channel in config.yaml.",
|
||||
)
|
||||
board = _resolve_board(board)
|
||||
conn = _conn(board=board)
|
||||
try:
|
||||
task = kanban_db.get_task(conn, task_id)
|
||||
if task is None:
|
||||
raise HTTPException(status_code=404, detail=f"task {task_id} not found")
|
||||
kanban_db.add_notify_sub(
|
||||
conn,
|
||||
task_id=task_id,
|
||||
platform=platform,
|
||||
chat_id=home["chat_id"],
|
||||
thread_id=home["thread_id"] or None,
|
||||
)
|
||||
return {"ok": True, "task_id": task_id, "home_channel": home}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}/home-subscribe/{platform}")
|
||||
def unsubscribe_home(task_id: str, platform: str, board: Optional[str] = Query(None)):
|
||||
"""Remove any notify subscription on *task_id* that matches *platform*'s home."""
|
||||
homes = _configured_home_channels()
|
||||
home = next((h for h in homes if h["platform"] == platform), None)
|
||||
if not home:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"No home channel configured for platform {platform!r}.",
|
||||
)
|
||||
board = _resolve_board(board)
|
||||
conn = _conn(board=board)
|
||||
try:
|
||||
kanban_db.remove_notify_sub(
|
||||
conn,
|
||||
task_id=task_id,
|
||||
platform=platform,
|
||||
chat_id=home["chat_id"],
|
||||
thread_id=home["thread_id"] or None,
|
||||
)
|
||||
return {"ok": True, "task_id": task_id, "home_channel": home}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stats (per-profile / per-status counts + oldest-ready age)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue