mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix channel task surface review issues
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user