mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:11:34 +00:00
fix(imessage): render complete poll selections [AI-assisted] (#114714)
* fix(imessage): render poll selection snapshots * fix(imessage): scope empty poll selections to sender --------- Co-authored-by: clawSean <260045960+clawSean@users.noreply.github.com> Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
@@ -728,7 +728,7 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
|
||||
function resolveIMessageInboundBodyText(message: IMessagePayload) {
|
||||
// Native poll balloons carry only a 0xFFFD placeholder in `text`; render the
|
||||
// decoded poll (question/options/votes) so the agent sees the actual poll.
|
||||
const pollBody = message.poll ? renderIMessagePollBody(message.poll) : null;
|
||||
const pollBody = message.poll ? renderIMessagePollBody(message.poll, message.sender) : null;
|
||||
const messageText = (pollBody ?? message.text ?? "").trim();
|
||||
const attachments = includeAttachments ? (message.attachments ?? []) : [];
|
||||
const effectiveAttachmentRoots = remoteHost ? remoteAttachmentRoots : attachmentRoots;
|
||||
|
||||
@@ -49,6 +49,70 @@ describe("renderIMessagePollBody", () => {
|
||||
expect(out).toContain("Blue");
|
||||
});
|
||||
|
||||
it("renders the full selection snapshot instead of a stale singular vote", () => {
|
||||
const out = renderIMessagePollBody({
|
||||
kind: "vote",
|
||||
vote: {
|
||||
participant: "+12065550123",
|
||||
option_id: "a",
|
||||
option_text: "Lobster",
|
||||
event_type: "selected",
|
||||
},
|
||||
votes: [
|
||||
{
|
||||
participant: "+12065550123",
|
||||
option_id: "a",
|
||||
option_text: "Lobster",
|
||||
event_type: "selected",
|
||||
},
|
||||
{
|
||||
participant: "+12065550123",
|
||||
option_id: "b",
|
||||
option_text: "Also lobster",
|
||||
event_type: "selected",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(out).toContain('currently selected "Lobster", "Also lobster"');
|
||||
expect(out).not.toContain('voted for "Lobster"');
|
||||
});
|
||||
|
||||
it("does not report a remaining selection as a new vote", () => {
|
||||
const out = renderIMessagePollBody({
|
||||
kind: "vote",
|
||||
vote: {
|
||||
participant: "+12065550123",
|
||||
option_id: "b",
|
||||
option_text: "Beta",
|
||||
event_type: "selected",
|
||||
},
|
||||
votes: [
|
||||
{
|
||||
participant: "+12065550123",
|
||||
option_id: "b",
|
||||
option_text: "Beta",
|
||||
event_type: "selected",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(out).toContain('currently selected "Beta"');
|
||||
expect(out).not.toContain('voted for "Beta"');
|
||||
});
|
||||
|
||||
it("scopes an empty selection snapshot to the event sender", () => {
|
||||
const out = renderIMessagePollBody(
|
||||
{
|
||||
kind: "vote",
|
||||
votes: [],
|
||||
},
|
||||
"+12065550123",
|
||||
);
|
||||
|
||||
expect(out).toBe("\u{1F4CA} Poll selections: +12065550123 currently selected no options");
|
||||
});
|
||||
|
||||
it("returns null for a poll with no options and no vote", () => {
|
||||
expect(renderIMessagePollBody({ kind: "created", options: [] })).toBeNull();
|
||||
});
|
||||
|
||||
@@ -6,16 +6,50 @@
|
||||
// numbered options so it can vote by 1-based index via the poll-vote action.
|
||||
import type { IMessagePoll } from "./types.js";
|
||||
|
||||
export function renderIMessagePollBody(poll: IMessagePoll): string | null {
|
||||
export function renderIMessagePollBody(poll: IMessagePoll, sender?: string | null): string | null {
|
||||
const options = poll.options ?? [];
|
||||
|
||||
// Vote update: surface who voted for what so the agent can follow tallies.
|
||||
// Vote update: prefer imsg's full selection snapshot. Its singular `vote`
|
||||
// field is only the first decoded entry, not necessarily the option that
|
||||
// changed. Older imsg builds without `votes` still use the singular fallback.
|
||||
if (poll.kind === "vote" || (poll.vote && options.length === 0)) {
|
||||
if (poll.votes) {
|
||||
// An empty snapshot carries no vote-level participant, but it still only
|
||||
// describes the event sender's selections—not the state of the full poll.
|
||||
const snapshotParticipant =
|
||||
poll.votes.find((vote) => vote.participant?.trim())?.participant?.trim() ||
|
||||
sender?.trim() ||
|
||||
"someone";
|
||||
const selectionsByParticipant = new Map<string, string[]>();
|
||||
for (const vote of poll.votes) {
|
||||
if (vote.event_type === "removed") {
|
||||
continue;
|
||||
}
|
||||
const who = vote.participant?.trim() || snapshotParticipant;
|
||||
const what = vote.option_text?.trim() || vote.option_id || "an option";
|
||||
const selections = selectionsByParticipant.get(who) ?? [];
|
||||
if (!selections.includes(what)) {
|
||||
selections.push(what);
|
||||
}
|
||||
selectionsByParticipant.set(who, selections);
|
||||
}
|
||||
|
||||
if (selectionsByParticipant.size === 0) {
|
||||
return `\u{1F4CA} Poll selections: ${snapshotParticipant} currently selected no options`;
|
||||
}
|
||||
|
||||
const snapshots = Array.from(selectionsByParticipant, ([who, selections]) => {
|
||||
const quotedSelections = selections.map((selection) => `"${selection}"`).join(", ");
|
||||
return `${who} currently selected ${quotedSelections}`;
|
||||
});
|
||||
return `\u{1F4CA} Poll selections: ${snapshots.join("; ")}`;
|
||||
}
|
||||
|
||||
const vote = poll.vote;
|
||||
if (!vote) {
|
||||
return "\u{1F4CA} Poll vote received";
|
||||
}
|
||||
const who = vote.participant?.trim() || "someone";
|
||||
const who = vote.participant?.trim() || sender?.trim() || "someone";
|
||||
const what = vote.option_text?.trim() || vote.option_id || "an option";
|
||||
const verb = vote.event_type === "removed" ? "removed their vote for" : "voted for";
|
||||
return `\u{1F4CA} Poll vote: ${who} ${verb} "${what}"`;
|
||||
|
||||
Reference in New Issue
Block a user