Fix task conversation review issues

This commit is contained in:
klopez4212
2026-06-29 21:42:49 +01:00
parent 24bc99f2a6
commit 35e2c32865
9 changed files with 136 additions and 18 deletions

View File

@@ -91,8 +91,8 @@ const overrides = new Map([
// applyWorkspace reposDir parameter plus the validateReposDir binding,
// threaded through Tauri invokes for configurable repos_dir, plus the
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
// parameter plumbing, not generic debt growth. Approved override; still
// queued to split.
// parameter plumbing, plus continued-agent-conversations client task-anchor
// tags on message sends. Approved override; still queued to split.
["src/shared/api/tauri.ts", 1235],
// harness-persona-sync feature growth, queued to split in the resolver-unify
// refactor followup. discovery.rs is dominated by the new test module

View File

@@ -283,6 +283,7 @@ pub async fn send_channel_message(
media_tags: Option<Vec<Vec<String>>>,
emoji_tags: Option<Vec<Vec<String>>>,
mention_tags: Option<Vec<Vec<String>>>,
client_tags: Option<Vec<Vec<String>>>,
mention_pubkeys: Option<Vec<String>>,
kind: Option<u32>,
state: State<'_, AppState>,
@@ -294,7 +295,11 @@ pub async fn send_channel_message(
let media = media_tags.unwrap_or_default();
let emoji = emoji_tags.unwrap_or_default();
let mention_refs_only = mention_tags.unwrap_or_default();
let client = client_tags.unwrap_or_default();
let kind_num = kind.unwrap_or(buzz_core_pkg::kind::KIND_STREAM_MESSAGE);
if kind_num != buzz_core_pkg::kind::KIND_STREAM_MESSAGE && !client.is_empty() {
return Err("client tags are only supported on stream messages".into());
}
let mut resolved_root: Option<String> = None;
@@ -330,7 +335,7 @@ pub async fn send_channel_message(
}
None => None,
};
events::build_message(
events::build_message_with_client_tags(
channel_uuid,
content.trim(),
thread_ref.as_ref(),
@@ -338,6 +343,7 @@ pub async fn send_channel_message(
&media,
&emoji,
&mention_refs_only,
&client,
)?
}
};

View File

@@ -556,6 +556,9 @@ export function AgentConversationScreen({
mediaTags?: string[][],
) => {
await sendMessageMutation.mutateAsync({
clientTags: [
["client", "agent-conversation", conversation.agentReply.id],
],
content,
mediaTags,
mentionPubkeys: buildAgentConversationMentionPubkeys({
@@ -565,7 +568,12 @@ export function AgentConversationScreen({
parentEventId: replyParentEventId,
});
},
[autoRoutedAgentPubkeys, replyParentEventId, sendMessageMutation],
[
autoRoutedAgentPubkeys,
conversation.agentReply.id,
replyParentEventId,
sendMessageMutation,
],
);
const isComposerDisabled =

View File

@@ -126,6 +126,7 @@ export function createOptimisticMessage(
mentionPubkeys: string[] = [],
parentEventId: string | null = null,
mediaTags: string[][] = [],
clientTags: string[][] = [],
): RelayEvent {
const localKey = `optimistic-${crypto.randomUUID()}`;
const tags: string[][] = [];
@@ -154,6 +155,9 @@ export function createOptimisticMessage(
for (const tag of mediaTags) {
tags.push(tag);
}
for (const tag of clientTags) {
tags.push(tag);
}
return {
id: localKey,
@@ -341,10 +345,12 @@ export function useSendMessageMutation(
mentionPubkeys?: string[];
parentEventId?: string | null;
mediaTags?: string[][];
clientTags?: string[][];
},
MessageQueryContext | undefined
>({
mutationFn: async ({
clientTags,
content,
mentionPubkeys,
parentEventId,
@@ -371,7 +377,12 @@ export function useSendMessageMutation(
// Messages carrying media OR custom-emoji tags MUST go through REST so
// the relay's tag validation runs. The WebSocket path emits no extra
// tags, so emoji-only messages would otherwise lose their emoji tag.
if (parentEventId || imetaTags.length > 0 || emojiTags.length > 0) {
if (
parentEventId ||
imetaTags.length > 0 ||
emojiTags.length > 0 ||
(clientTags?.length ?? 0) > 0
) {
const cachedMessages =
queryClient.getQueryData<RelayEvent[]>(
channelMessagesKey(channel.id),
@@ -385,6 +396,7 @@ export function useSendMessageMutation(
undefined,
emojiTags,
mentionTags,
clientTags,
);
// Build tags matching relay-emitted shape: h, author p, mention ps, reply es, imeta, emoji.
@@ -423,6 +435,7 @@ export function useSendMessageMutation(
...imetaTags,
...emojiTags,
...mentionTags,
...(clientTags ?? []),
],
content: content.trim(),
sig: "",
@@ -436,7 +449,13 @@ export function useSendMessageMutation(
mentionTags,
);
},
onMutate: async ({ content, mentionPubkeys, parentEventId, mediaTags }) => {
onMutate: async ({
clientTags,
content,
mentionPubkeys,
parentEventId,
mediaTags,
}) => {
if (!channel || !identity || channel.channelType === "forum") {
return undefined;
}
@@ -454,6 +473,7 @@ export function useSendMessageMutation(
mentionPubkeys ?? [],
parentEventId ?? null,
mediaTags ?? [],
clientTags ?? [],
);
queryClient.setQueryData<RelayEvent[]>(

View File

@@ -883,8 +883,7 @@ export const MessageRow = React.memo(
prev.onCollapseDescendants === next.onCollapseDescendants &&
prev.onCollapseDescendantsHoverChange ===
next.onCollapseDescendantsHoverChange &&
Boolean(prev.onOpenAgentConversation) ===
Boolean(next.onOpenAgentConversation) &&
prev.onOpenAgentConversation === next.onOpenAgentConversation &&
prev.profiles === next.profiles &&
prev.searchQuery === next.searchQuery &&
prev.videoReviewContext === next.videoReviewContext,

View File

@@ -265,6 +265,7 @@ const MessageTimelineBase = React.forwardRef<
isLoading: showTimelineSkeleton,
messages: deferredMessages,
onTargetReached,
resetKey: scrollRouteKey,
scrollContainerRef,
targetMessageId,
});

View File

@@ -40,6 +40,8 @@ type UseAnchoredScrollOptions = {
contentRef: React.RefObject<HTMLDivElement | null>;
/** Resets when changed; lets us drop anchor + scroll state across channels. */
channelId?: string | null;
/** Resets when changed; includes channel plus route-specific layout state. */
resetKey?: string | null;
/** Suppresses initial scroll-to-bottom while a skeleton is showing. */
isLoading: boolean;
/** Source of truth for the rendered list. Used to detect new-at-bottom
@@ -145,6 +147,7 @@ export function useAnchoredScroll({
scrollContainerRef,
contentRef,
channelId,
resetKey = channelId ?? null,
isLoading,
messages,
@@ -181,10 +184,10 @@ export function useAnchoredScroll({
// guard runs on a native scroll event, outside React's render cycle.
const settlingRef = React.useRef(false);
// Reset everything when the channel changes — the layout effect that runs
// immediately after this reset is responsible for either jumping to bottom
// or to the target message for the new channel.
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is intentionally the sole trigger — we want this effect to fire exactly when the channel changes (and on mount).
// Reset everything when the route's scroll identity changes — the layout
// effect that runs immediately after this reset is responsible for either
// jumping to bottom or to the target message for the new view.
// biome-ignore lint/correctness/useExhaustiveDependencies: resetKey is intentionally the sole trigger — it includes channel identity plus route-specific layout state.
React.useLayoutEffect(() => {
anchorRef.current = { kind: "at-bottom" };
setIsAtBottom(true);
@@ -205,7 +208,7 @@ export function useAnchoredScroll({
cancelAnimationFrame(mountPinRafIdRef.current);
mountPinRafIdRef.current = null;
}
}, [channelId]);
}, [resetKey]);
const scrollToBottomImperative = React.useCallback(
(behavior: ScrollBehavior = "auto") => {
@@ -454,7 +457,7 @@ export function useAnchoredScroll({
// mid-history, native scroll anchoring (overflow-anchor) holds the reading
// row across the reflow, so there's nothing to do.
// ---------------------------------------------------------------------------
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is a deliberate re-subscription trigger — the effect body reads only the stable refs, but on a channel switch the keyed scroll container remounts and contentRef.current becomes a fresh node, so the observer must disconnect from the previous channel's detached node and re-observe the live one.
// biome-ignore lint/correctness/useExhaustiveDependencies: resetKey is a deliberate re-subscription trigger — the effect body reads only the stable refs, but on route identity changes the keyed scroll container remounts and contentRef.current becomes a fresh node, so the observer must disconnect from the previous route's detached node and re-observe the live one.
React.useEffect(() => {
const content = contentRef.current;
if (!content || typeof ResizeObserver === "undefined") return;
@@ -467,7 +470,7 @@ export function useAnchoredScroll({
});
observer.observe(content);
return () => observer.disconnect();
}, [channelId, contentRef, scrollContainerRef]);
}, [resetKey, contentRef, scrollContainerRef]);
// ---------------------------------------------------------------------------
// Target message handling (deep link, jump-to-reply, etc.). Distinct from

View File

@@ -717,6 +717,7 @@ export async function sendChannelMessage(
kind?: number,
emojiTags?: string[][],
mentionTags?: string[][],
clientTags?: string[][],
): Promise<SendChannelMessageResult> {
const response = await invokeTauri<RawSendChannelMessageResult>(
"send_channel_message",
@@ -727,6 +728,7 @@ export async function sendChannelMessage(
mediaTags: mediaTags ?? null,
emojiTags: emojiTags ?? null,
mentionTags: mentionTags ?? null,
clientTags: clientTags ?? null,
mentionPubkeys: mentionPubkeys ?? null,
kind: kind ?? null,
},