Fix channel task surface review issues

This commit is contained in:
klopez4212
2026-06-30 13:55:15 +01:00
parent 85655c0192
commit c1cd1f133c
5 changed files with 54 additions and 42 deletions
+1 -1
View File
@@ -147,7 +147,7 @@ const overrides = new Map([
// continued-agent-conversations: channel task-tab state, deep-link task
// routing, and side-panel suppression sit at the channel orchestration seam.
// latest main rebase threads additional header routing through this seam.
["src/features/channels/ui/ChannelScreen.tsx", 1027],
["src/features/channels/ui/ChannelScreen.tsx", 1040],
// continued-agent-conversations: composer notice banner for read-only agent
// conversations.
["src/features/messages/ui/MessageComposer.tsx", 1010],
@@ -22,3 +22,16 @@ export function buildAgentConversationLink(
return `${AGENT_CONVERSATION_LINK_SCHEME}//${AGENT_CONVERSATION_LINK_HOST}?${params.toString()}`;
}
export function isAgentConversationLink(
href: string | undefined | null,
): boolean {
if (!href) return false;
return (
href.startsWith(
`${AGENT_CONVERSATION_LINK_SCHEME}//${AGENT_CONVERSATION_LINK_HOST}?`,
) ||
href ===
`${AGENT_CONVERSATION_LINK_SCHEME}//${AGENT_CONVERSATION_LINK_HOST}`
);
}
@@ -80,29 +80,10 @@ import { cn } from "@/shared/lib/cn";
type ChannelTaskItem = {
marker: AgentConversationMarker;
message: TimelineMessage;
threadMessage: TimelineMessage;
message: TimelineMessage | null;
threadMessage: TimelineMessage | null;
};
function buildTaskFallbackMessage(
marker: AgentConversationMarker,
): TimelineMessage {
const createdAt = marker.startedAt || marker.createdAt;
return {
author: marker.agentName,
avatarUrl: null,
body: "",
createdAt,
depth: 0,
id: marker.agentReplyId,
parentId: marker.parentMessageId,
pubkey: marker.agentPubkey,
rootId: marker.threadRootId,
time: formatTime(createdAt),
};
}
function formatTaskStartedAt(unixSeconds: number): string {
return `${formatDayHeading(unixSeconds)} at ${formatTime(unixSeconds)}`;
}
@@ -118,7 +99,7 @@ function ChannelTaskRow({
}: {
currentPubkey?: string;
marker: AgentConversationMarker;
message: TimelineMessage;
message: TimelineMessage | null;
onOpenAgentConversation?: (
message: TimelineMessage,
options?: { publishMarker?: boolean },
@@ -129,7 +110,7 @@ function ChannelTaskRow({
threadMessage: TimelineMessage,
) => void;
profiles?: UserProfileLookup;
threadMessage: TimelineMessage;
threadMessage: TimelineMessage | null;
}) {
const startedAt = marker.startedAt || marker.createdAt;
const starterName = resolveUserLabel({
@@ -166,8 +147,12 @@ function ChannelTaskRow({
<Button
className="h-8 rounded-lg px-3 text-xs font-medium"
data-testid="channel-task-go-to-thread"
disabled={!onGoToTaskMessage}
onClick={() => onGoToTaskMessage?.(marker, message, threadMessage)}
disabled={!onGoToTaskMessage || !message || !threadMessage}
onClick={() => {
if (message && threadMessage) {
onGoToTaskMessage?.(marker, message, threadMessage);
}
}}
title="Go to source message in channel"
type="button"
variant="secondary"
@@ -177,10 +162,12 @@ function ChannelTaskRow({
<Button
className="h-8 rounded-lg px-3 text-xs font-medium"
data-testid="channel-task-open"
disabled={!onOpenAgentConversation}
onClick={() =>
onOpenAgentConversation?.(message, { publishMarker: false })
}
disabled={!onOpenAgentConversation || !message}
onClick={() => {
if (message) {
onOpenAgentConversation?.(message, { publishMarker: false });
}
}}
type="button"
variant="outline"
>
@@ -228,14 +215,13 @@ function ChannelTasksView({
return (agentConversationMarkers ?? [])
.filter((marker) => !channelId || marker.channelId === channelId)
.map((marker) => {
const message =
messageById.get(marker.agentReplyId) ??
buildTaskFallbackMessage(marker);
const message = messageById.get(marker.agentReplyId) ?? null;
const threadMessage =
messageById.get(marker.threadRootMessageId ?? "") ??
messageById.get(marker.threadRootId) ??
messageById.get(marker.parentMessageId ?? "") ??
message;
message ??
null;
return {
marker,
message,
+12 -3
View File
@@ -417,9 +417,9 @@ test("rehypeImageGallery: mixed content paragraph is not image-only", () => {
// Regression test: react-markdown's `defaultUrlTransform` strips unknown
// schemes (returns `""`) before our `a` component override can see them,
// which would break copy → paste → click for `buzz://message?…` links
// which would break copy → paste → click for internal `buzz://…` links
// end-to-end. We pass a custom `urlTransform` that delegates to the
// default for `buzz://message` and legacy `buzz://message` hrefs.
// default for external/unsafe hrefs.
//
// This test renders real `<ReactMarkdown>` with the production transform
// and asserts the link href survives to the rendered DOM. Mirrors the
@@ -429,11 +429,15 @@ import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import { isAgentConversationLink } from "../../features/agents/agentConversationLink.ts";
import { isMessageLink } from "../../features/messages/lib/messageLink.ts";
import remarkSpoilers from "../lib/remarkSpoilers.ts";
function messageLinkUrlTransform(value, key) {
if (key === "href" && isMessageLink(value)) {
if (
key === "href" &&
(isMessageLink(value) || isAgentConversationLink(value))
) {
return value;
}
return defaultUrlTransform(value);
@@ -469,6 +473,11 @@ test("messageLinkUrlTransform: preserves buzz://message href with thread", () =>
assert.match(html, /href="buzz:\/\/message\?[^"]*thread=t1"/);
});
test("messageLinkUrlTransform: preserves buzz://task href", () => {
const html = renderMarkdown("[task](buzz://task?channel=c1&reply=m1)");
assert.match(html, /href="buzz:\/\/task\?channel=c1&(?:amp;)?reply=m1"/);
});
test("messageLinkUrlTransform: still strips javascript: scheme", () => {
const html = renderMarkdown("[xss](javascript:alert(1))");
// defaultUrlTransform replaces unsafe schemes with the empty string.
+9 -5
View File
@@ -1,6 +1,7 @@
import * as React from "react";
import { defaultUrlTransform } from "react-markdown";
import { isAgentConversationLink } from "@/features/agents/agentConversationLink";
import { isMessageLink } from "@/features/messages/lib/messageLink";
export function useStableArray<T>(arr: T[]): T[] {
@@ -60,13 +61,16 @@ export function isInsideHiddenSpoiler(element: Element): boolean {
}
/**
* `urlTransform` for `<ReactMarkdown>` that preserves `buzz://message?…`
* links. The default transform strips unknown schemes (returns `""`) before
* the `a` component override can see them, which would break copy → paste →
* click end-to-end. Everything else delegates to `defaultUrlTransform`.
* `urlTransform` for `<ReactMarkdown>` that preserves internal Buzz links.
* The default transform strips unknown schemes (returns `""`) before the `a`
* component override can see them, which would break copy → paste → click
* end-to-end. Everything else delegates to `defaultUrlTransform`.
*/
export function messageLinkUrlTransform(value: string, key: string): string {
if (key === "href" && isMessageLink(value)) {
if (
key === "href" &&
(isMessageLink(value) || isAgentConversationLink(value))
) {
return value;
}
return defaultUrlTransform(value);