feat: update trigger ui

This commit is contained in:
Richard Tang
2026-04-20 11:19:57 -07:00
parent 3c91119f67
commit 24bcc5aea7
7 changed files with 123 additions and 2 deletions
+10
View File
@@ -61,6 +61,11 @@ class Message:
# compacted-summary message it writes when a colony is born from a
# queen DM. Presence of the field IS the "inherited" signal.
inherited_from: str | None = None
# True when this user message was synthesized from one or more
# fired triggers (timer/webhook), not typed by a human. The LLM still
# sees the message as a regular user turn; the UI uses this flag to
# render it as a trigger banner instead of a speech bubble.
is_trigger: bool = False
def to_llm_dict(self) -> dict[str, Any]:
"""Convert to OpenAI-format message dict."""
@@ -128,6 +133,8 @@ class Message:
d["truncated"] = self.truncated
if self.inherited_from is not None:
d["inherited_from"] = self.inherited_from
if self.is_trigger:
d["is_trigger"] = self.is_trigger
return d
@classmethod
@@ -148,6 +155,7 @@ class Message:
is_system_nudge=data.get("is_system_nudge", False),
truncated=data.get("truncated", False),
inherited_from=data.get("inherited_from"),
is_trigger=data.get("is_trigger", False),
)
@@ -493,6 +501,7 @@ class NodeConversation:
is_client_input: bool = False,
image_content: list[dict[str, Any]] | None = None,
is_system_nudge: bool = False,
is_trigger: bool = False,
) -> Message:
msg = Message(
seq=self._next_seq,
@@ -504,6 +513,7 @@ class NodeConversation:
is_client_input=is_client_input,
image_content=image_content,
is_system_nudge=is_system_nudge,
is_trigger=is_trigger,
)
self._messages.append(msg)
self._next_seq += 1
@@ -234,7 +234,9 @@ async def drain_trigger_queue(
combined = "\n\n".join(parts)
logger.info("[drain] %d trigger(s): %s", len(triggers), combined[:200])
await conversation.add_user_message(combined)
# Tag the message so the UI can render a banner instead of the raw
# `[TRIGGER: ...]` text. The LLM still sees `combined` verbatim.
await conversation.add_user_message(combined, is_trigger=True)
return len(triggers)
+1
View File
@@ -209,6 +209,7 @@ async def handle_events(request: web.Request) -> web.StreamResponse:
EventType.TRIGGER_AVAILABLE.value,
EventType.TRIGGER_ACTIVATED.value,
EventType.TRIGGER_DEACTIVATED.value,
EventType.TRIGGER_FIRED.value,
EventType.TRIGGER_REMOVED.value,
EventType.TRIGGER_UPDATED.value,
}
@@ -493,9 +493,17 @@ async def _emit_trigger_fired(session: Any, trigger_id: str, trigger_type: str)
from framework.host.event_bus import AgentEvent, EventType
# Pull the task/description off the trigger definition so the chat
# banner can render something human-readable without a second fetch.
tdef = getattr(session, "available_triggers", {}).get(trigger_id)
task_str = getattr(tdef, "task", "") or "" if tdef else ""
name_str = getattr(tdef, "description", "") or trigger_id if tdef else trigger_id
data: dict[str, Any] = {
"trigger_id": trigger_id,
"trigger_type": trigger_type,
"name": name_str,
"task": task_str,
"last_fired_at": last_fired_at,
}
if fire_count is not None:
+50 -1
View File
@@ -9,6 +9,7 @@ import {
Loader2,
Paperclip,
X,
Zap,
} from "lucide-react";
import WorkerRunBubble from "@/components/WorkerRunBubble";
import type { WorkerRunGroup } from "@/components/WorkerRunBubble";
@@ -54,7 +55,8 @@ export interface ChatMessage {
| "worker_input_request"
| "run_divider"
| "colony_link"
| "inherited_block";
| "inherited_block"
| "trigger";
role?: "queen" | "worker";
/** Which worker thread this message belongs to (worker agent name) */
thread?: string;
@@ -635,6 +637,53 @@ const MessageBubble = memo(
);
}
if (msg.type === "trigger") {
// Rendered when a scheduler/webhook trigger fires. Content is a JSON
// payload: { trigger_id, trigger_type, name, task, last_fired_at,
// fire_count }. Shown as a distinctive banner marking the start of
// the turn the queen is about to run in response.
let parsed: {
trigger_id?: string;
trigger_type?: string;
name?: string;
task?: string;
fire_count?: number;
last_fired_at?: number;
} = {};
try {
parsed = JSON.parse(msg.content);
} catch {
// Fall through to plain text
}
const label = parsed.name || parsed.trigger_id || "trigger";
const kind = parsed.trigger_type || "timer";
const task = (parsed.task || "").trim();
const fireCount = parsed.fire_count;
return (
<div className="flex justify-center py-2">
<div className="max-w-[85%] w-full rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2">
<div className="flex items-center gap-2 mb-1">
<span className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-amber-500/15 text-amber-400">
<Zap className="w-3 h-3" />
</span>
<span className="text-[11px] font-semibold text-amber-400 uppercase tracking-wider">
{kind === "webhook" ? "Webhook" : "Scheduler"} fired
</span>
<span className="text-[11px] text-foreground font-mono truncate">{label}</span>
{fireCount != null && fireCount > 0 && (
<span className="ml-auto text-[10px] text-muted-foreground">#{fireCount}</span>
)}
</div>
{task && (
<p className="text-[12px] text-muted-foreground leading-snug whitespace-pre-wrap">
{task}
</p>
)}
</div>
</div>
);
}
if (msg.type === "colony_link") {
// Rendered when the queen calls create_colony() and the backend
// emits a COLONY_CREATED event. Gives the user a clickable card
+28
View File
@@ -211,6 +211,34 @@ export function sseEventToChatMessage(
};
}
case "trigger_fired": {
// Surface each scheduler/webhook fire as a banner in the chat, so the
// user can see exactly when the queen was invoked by a trigger vs. by
// a typed message. The banner sits at the start of the turn the queen
// is about to run in response.
const triggerId = event.data?.trigger_id as string | undefined;
if (!triggerId) return null;
const payload = {
trigger_id: triggerId,
trigger_type: event.data?.trigger_type as string | undefined,
name: event.data?.name as string | undefined,
task: event.data?.task as string | undefined,
fire_count: event.data?.fire_count as number | undefined,
last_fired_at: event.data?.last_fired_at as number | undefined,
};
return {
id: `trigger-${triggerId}-${payload.last_fired_at ?? event.timestamp}`,
agent: "Trigger",
agentColor: "",
content: JSON.stringify(payload),
timestamp: "",
type: "trigger",
thread,
createdAt,
streamId: event.stream_id || undefined,
};
}
default:
return null;
}
+23
View File
@@ -1190,6 +1190,29 @@ export default function ColonyChat() {
);
updateGraphNodeStatus(nodeId, "complete");
setTimeout(() => updateGraphNodeStatus(nodeId, "running"), 1500);
// Render a banner in the chat marking the start of the turn the
// queen is about to run in response. Matches the replay path in
// chat-helpers.ts (case "trigger_fired") so live + restore look
// identical.
const bannerPayload = {
trigger_id: triggerId,
trigger_type: event.data?.trigger_type as string | undefined,
name: event.data?.name as string | undefined,
task: event.data?.task as string | undefined,
fire_count: fireCount,
last_fired_at: lastFiredAt,
};
upsertMessage({
id: `trigger-${triggerId}-${lastFiredAt ?? event.timestamp}`,
agent: "Trigger",
agentColor: "",
content: JSON.stringify(bannerPayload),
timestamp: "",
type: "trigger",
thread: agentPath,
createdAt: lastFiredAt ?? Date.now(),
});
}
break;
}