;
channel: Channel | null;
channelId: string | null;
channelName: string;
@@ -295,7 +294,6 @@ export function MessageThreadPanelSkeleton({
}
export function MessageThreadPanel({
- agentPubkeys,
channel,
channelId,
channelName,
@@ -616,7 +614,6 @@ export function MessageThreadPanel({
{showUnreadDivider ? : null}
;
channelId?: string | null;
channelIntro?: ChannelIntro | null;
channelName?: string;
@@ -141,7 +140,6 @@ const MessageTimelineBase = React.forwardRef<
MessageTimelineProps
>(function MessageTimeline(
{
- agentPubkeys,
channelId,
channelIntro = null,
directMessageIntro = null,
@@ -581,7 +579,6 @@ const MessageTimelineBase = React.forwardRef<
>
;
channelId?: string | null;
channelName?: string;
channelType?: ChannelType | null;
@@ -84,7 +83,6 @@ type TimelineMessageListProps = {
};
export const TimelineMessageList = React.memo(function TimelineMessageList({
- agentPubkeys,
channelId,
channelName,
channelType,
@@ -196,7 +194,6 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
case "message":
return (
AGENT_PUBKEYS.has(pubkey);
function makeEvent(overrides = {}) {
return {
@@ -80,7 +84,7 @@ test("signerIsHuman_actorTagAttributedToAgent_returnsUndefined", () => {
// The guard must reject: signer is human, not in AGENT_PUBKEYS.
assert.equal(
- getConfigNudgeAuthorPubkey(msg, AGENT_PUBKEYS),
+ getConfigNudgeAuthorPubkey(msg, isKnownAgentPubkey),
undefined,
"human signer with actor-tag attribution to agent must NOT enable the card",
);
@@ -103,7 +107,7 @@ test("signerIsAgent_genuine_returnsAgentPubkey", () => {
);
assert.equal(
- getConfigNudgeAuthorPubkey(msg, AGENT_PUBKEYS),
+ getConfigNudgeAuthorPubkey(msg, isKnownAgentPubkey),
AGENT_PUBKEY,
"genuine agent-signed kind:9 must enable the card",
);
@@ -123,7 +127,7 @@ test("nonKind9_agentSigner_returnsUndefined", () => {
const [msg] = format(event);
assert.equal(
- getConfigNudgeAuthorPubkey(msg, AGENT_PUBKEYS),
+ getConfigNudgeAuthorPubkey(msg, isKnownAgentPubkey),
undefined,
"non-kind:9 events must never enable the card even if signer is known agent",
);
diff --git a/desktop/src/features/messages/ui/configNudgeAuthPubkey.ts b/desktop/src/features/messages/ui/configNudgeAuthPubkey.ts
index 1866e59040..433ff0ffa7 100644
--- a/desktop/src/features/messages/ui/configNudgeAuthPubkey.ts
+++ b/desktop/src/features/messages/ui/configNudgeAuthPubkey.ts
@@ -8,9 +8,12 @@ import type { TimelineMessage } from "@/features/messages/types";
* The card is enabled ONLY when:
* 1. `message.kind === KIND_STREAM_MESSAGE` — restricts to the setup-listener
* wire format.
- * 2. `message.signerPubkey` is set and is a known agent — authenticates
- * against the raw event signer (NOT `message.pubkey`, which is the
- * tag-attributed display author and can be spoofed via `actor`/`p` tags).
+ * 2. `message.signerPubkey` is set and passes `isKnownAgentPubkey` —
+ * authenticates against the raw event signer (NOT `message.pubkey`,
+ * which is the tag-attributed display author and can be spoofed via
+ * `actor`/`p` tags). The caller's predicate combines the workspace-wide
+ * known-agent baseline (`useKnownAgentPubkeys`) with any surface-local
+ * signals such as the signer profile's `isAgent` flag.
*
* Extracting this predicate as a pure helper lets tests exercise the exact
* signer-vs-attributed-author distinction with a real `TimelineMessage` from
@@ -18,12 +21,12 @@ import type { TimelineMessage } from "@/features/messages/types";
*/
export function getConfigNudgeAuthorPubkey(
message: Pick,
- resolvedAgentPubkeys: ReadonlySet,
+ isKnownAgentPubkey: (pubkey: string) => boolean,
): string | undefined {
if (
message.kind === KIND_STREAM_MESSAGE &&
message.signerPubkey &&
- resolvedAgentPubkeys.has(message.signerPubkey)
+ isKnownAgentPubkey(message.signerPubkey)
) {
return message.signerPubkey;
}
diff --git a/desktop/src/shared/hooks/useStableReference.ts b/desktop/src/shared/hooks/useStableReference.ts
index e02bca4032..fe99ac610f 100644
--- a/desktop/src/shared/hooks/useStableReference.ts
+++ b/desktop/src/shared/hooks/useStableReference.ts
@@ -52,3 +52,27 @@ function arraysShallowEqual(
}
return true;
}
+
+/**
+ * Returns `next` but preserves the previous reference when the two Sets have
+ * identical membership. Same purpose as `useStableMap` for Set-valued derived
+ * state (e.g. pubkey sets rebuilt whenever a polling query re-materialises
+ * its data without changing which pubkeys are in it).
+ */
+export function useStableSet(next: ReadonlySet): ReadonlySet {
+ const ref = React.useRef(next);
+ const prev = ref.current;
+ if (prev !== next && setsEqual(prev, next)) {
+ return prev;
+ }
+ ref.current = next;
+ return next;
+}
+
+function setsEqual(a: ReadonlySet, b: ReadonlySet): boolean {
+ if (a.size !== b.size) return false;
+ for (const value of a) {
+ if (!b.has(value)) return false;
+ }
+ return true;
+}