mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
feat(tui_gateway): emit notification.show on background-process completion (Option B)
A notify_on_complete background process (the agent's terminal tool, proc_*) only reached the TUI as the AGENT'S NARRATION — the completion is fed to the model as a synthetic prompt (_run_prompt_submit) and the gateway emitted only message.start + the reply, never the completion itself. So the OpenTUI card mechanism had nothing to render and the synthetic turn read as a context-less line. Additive fix (glitch-approved relaxation of the no-core rule for this one emit): new _emit_process_completion_card() fires a `notification.show` (text "<cmd> exited <code>", level info/warn by exit code, kind process.complete, key proc:<id>) at the two completion-delivery sites, just before the agent turn. The OpenTUI engine renders it as a distinct inline card (P1) + an OSC ping; Ink treats it as a notice. Completion events only (watch matches skipped); call-site dedup → one card per exit. No existing behavior changes — the agent turn still happens. Gateway suite green (321 passed incl. 5 new TestProcessCompletionCard cases).
This commit is contained in:
parent
8e853e3ff8
commit
21c64f90aa
2 changed files with 84 additions and 0 deletions
|
|
@ -7856,3 +7856,51 @@ def test_reap_idle_sessions_closes_only_evictable(monkeypatch):
|
|||
assert closed == [("stale", "idle_timeout")]
|
||||
finally:
|
||||
server._sessions.clear()
|
||||
|
||||
|
||||
class TestProcessCompletionCard:
|
||||
"""_emit_process_completion_card surfaces a background-process completion to
|
||||
the TUI as a notification.show card (Option B, glitch 2026-06-14) — in
|
||||
addition to the agent turn the completion triggers."""
|
||||
|
||||
@staticmethod
|
||||
def _capture(monkeypatch):
|
||||
emitted: list = []
|
||||
monkeypatch.setattr(server, "_emit", lambda event, sid, payload=None: emitted.append((event, sid, payload)))
|
||||
return emitted
|
||||
|
||||
def test_completion_exit_zero_is_an_info_card(self, monkeypatch):
|
||||
emitted = self._capture(monkeypatch)
|
||||
server._emit_process_completion_card(
|
||||
"s1", {"type": "completion", "session_id": "proc_1", "command": "sleep 20 && echo hi", "exit_code": 0}
|
||||
)
|
||||
assert len(emitted) == 1
|
||||
event, sid, payload = emitted[0]
|
||||
assert event == "notification.show"
|
||||
assert sid == "s1"
|
||||
assert payload["text"] == "sleep 20 && echo hi exited 0"
|
||||
assert payload["level"] == "info"
|
||||
assert payload["kind"] == "process.complete"
|
||||
assert payload["key"] == "proc:proc_1"
|
||||
|
||||
def test_nonzero_exit_is_a_warn_card(self, monkeypatch):
|
||||
emitted = self._capture(monkeypatch)
|
||||
server._emit_process_completion_card("s1", {"type": "completion", "command": "build", "exit_code": 1, "session_id": "p2"})
|
||||
assert emitted[0][2]["level"] == "warn"
|
||||
assert emitted[0][2]["text"] == "build exited 1"
|
||||
|
||||
def test_watch_match_is_not_carded(self, monkeypatch):
|
||||
emitted = self._capture(monkeypatch)
|
||||
server._emit_process_completion_card("s1", {"type": "watch_match", "command": "tail -f log"})
|
||||
assert emitted == []
|
||||
|
||||
def test_long_command_is_truncated(self, monkeypatch):
|
||||
emitted = self._capture(monkeypatch)
|
||||
server._emit_process_completion_card("s1", {"type": "completion", "command": "x" * 100, "exit_code": 0, "session_id": "p3"})
|
||||
assert "…" in emitted[0][2]["text"]
|
||||
assert len(emitted[0][2]["text"]) < 80
|
||||
|
||||
def test_missing_exit_code_says_finished(self, monkeypatch):
|
||||
emitted = self._capture(monkeypatch)
|
||||
server._emit_process_completion_card("s1", {"type": "completion", "command": "daemon", "session_id": "p4"})
|
||||
assert emitted[0][2]["text"] == "daemon finished"
|
||||
|
|
|
|||
|
|
@ -5607,6 +5607,39 @@ def _notification_event_dedup_key(evt: dict) -> tuple:
|
|||
return (evt_sid, evt_type)
|
||||
|
||||
|
||||
def _emit_process_completion_card(sid: str, evt: dict) -> None:
|
||||
"""Surface a background-process COMPLETION to the TUI as a notification card,
|
||||
in ADDITION to the agent turn it triggers. A bare `notify_on_complete` exit
|
||||
otherwise reaches the TUI only as the agent's narration (the completion is fed
|
||||
to the model as a synthetic prompt, never sent to the UI). Emitting a
|
||||
``notification.show`` lets the OpenTUI engine render a distinct inline card so
|
||||
the user actually sees the process finish; the Ink engine treats it as a
|
||||
notice. Additive — no existing behaviour changes. Completion events only
|
||||
(watch matches aren't terminal); the dedup at the call sites ensures one card
|
||||
per completion. (glitch 2026-06-14)"""
|
||||
if evt.get("type", "completion") != "completion":
|
||||
return
|
||||
cmd = str(evt.get("command") or "process").strip().replace("\n", " ")
|
||||
if len(cmd) > 60:
|
||||
cmd = cmd[:59] + "…"
|
||||
code = evt.get("exit_code")
|
||||
if code is None:
|
||||
text, level = f"{cmd} finished", "info"
|
||||
else:
|
||||
text = f"{cmd} exited {code}"
|
||||
level = "info" if code == 0 else "warn"
|
||||
_emit(
|
||||
"notification.show",
|
||||
sid,
|
||||
{
|
||||
"text": text,
|
||||
"kind": "process.complete",
|
||||
"level": level,
|
||||
"key": f"proc:{evt.get('session_id', '')}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _notification_poller_loop(
|
||||
stop_event: threading.Event, sid: str, session: dict
|
||||
) -> None:
|
||||
|
|
@ -5665,6 +5698,7 @@ def _notification_poller_loop(
|
|||
|
||||
rid = f"__notif__{int(time.time() * 1000)}"
|
||||
try:
|
||||
_emit_process_completion_card(sid, evt)
|
||||
_emit("message.start", sid)
|
||||
_run_prompt_submit(rid, sid, session, text)
|
||||
except Exception as exc:
|
||||
|
|
@ -5708,6 +5742,7 @@ def _notification_poller_loop(
|
|||
|
||||
rid = f"__notif__{int(time.time() * 1000)}"
|
||||
try:
|
||||
_emit_process_completion_card(sid, evt)
|
||||
_emit("message.start", sid)
|
||||
_run_prompt_submit(rid, sid, session, text)
|
||||
except Exception as exc:
|
||||
|
|
@ -6148,6 +6183,7 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
|
|||
break
|
||||
session["running"] = True
|
||||
try:
|
||||
_emit_process_completion_card(sid, _evt)
|
||||
_emit("message.start", sid)
|
||||
_run_prompt_submit(rid, sid, session, synth)
|
||||
except Exception as _n_exc:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue