mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Autolink bare task URLs
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
const AGENT_CONVERSATION_LINK_SCHEME = "buzz:";
|
||||
const AGENT_CONVERSATION_LINK_HOST = "task";
|
||||
export const AGENT_CONVERSATION_LINK_URL_PATTERN =
|
||||
/buzz:\/\/task\?[^\s<>"')\]]+/g;
|
||||
const TRAILING_PUNCTUATION_PATTERN = /[.,;:!?]+$/;
|
||||
|
||||
export type AgentConversationLinkInput = {
|
||||
agentReplyId: string;
|
||||
@@ -79,3 +82,17 @@ export function isAgentConversationLink(
|
||||
`${AGENT_CONVERSATION_LINK_SCHEME}//${AGENT_CONVERSATION_LINK_HOST}`
|
||||
);
|
||||
}
|
||||
|
||||
function isUnmatchedClosing(value: string): boolean {
|
||||
const closing = value[value.length - 1];
|
||||
const opening = closing === ")" ? "(" : "[";
|
||||
return value.split(closing).length > value.split(opening).length;
|
||||
}
|
||||
|
||||
export function trimAgentConversationLinkMatch(matchText: string) {
|
||||
let value = matchText.replace(TRAILING_PUNCTUATION_PATTERN, "");
|
||||
while (/[)\]]$/.test(value) && isUnmatchedClosing(value)) {
|
||||
value = value.slice(0, -1).replace(TRAILING_PUNCTUATION_PATTERN, "");
|
||||
}
|
||||
return { value, trailing: matchText.slice(value.length) };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Remark plugin that detects bare `buzz://task?…` URLs and replaces each with
|
||||
* a custom task-link element. The renderer turns that element into an in-app
|
||||
* task link instead of leaving a raw custom-scheme URL as inert text.
|
||||
*/
|
||||
|
||||
import { createRemarkPrefixPlugin } from "../../shared/lib/createRemarkPrefixPlugin.ts";
|
||||
import {
|
||||
AGENT_CONVERSATION_LINK_URL_PATTERN,
|
||||
trimAgentConversationLinkMatch,
|
||||
} from "./agentConversationLink.ts";
|
||||
|
||||
export default function remarkAgentConversationLinks() {
|
||||
return createRemarkPrefixPlugin(
|
||||
AGENT_CONVERSATION_LINK_URL_PATTERN,
|
||||
(matchText) => {
|
||||
const { value, trailing } = trimAgentConversationLinkMatch(matchText);
|
||||
|
||||
return {
|
||||
node: {
|
||||
type: "agent-conversation-link",
|
||||
value,
|
||||
data: {
|
||||
hName: "agent-conversation-link",
|
||||
hChildren: [{ type: "text", value }],
|
||||
},
|
||||
},
|
||||
trailing,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -535,12 +535,18 @@ test("remarkSpoilers: block delimiter spoilers expose a block prop to React", ()
|
||||
// the rendering side is a plain React component covered by app-level use.
|
||||
|
||||
import remarkMessageLinks from "../../features/messages/lib/remarkMessageLinks.ts";
|
||||
import remarkAgentConversationLinks from "../../features/agents/remarkAgentConversationLinks.ts";
|
||||
|
||||
function runPlugin(tree) {
|
||||
remarkMessageLinks()(tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
function runTaskPlugin(tree) {
|
||||
remarkAgentConversationLinks()(tree);
|
||||
return tree;
|
||||
}
|
||||
|
||||
function paragraph(...children) {
|
||||
return { type: "root", children: [{ type: "paragraph", children }] };
|
||||
}
|
||||
@@ -665,3 +671,33 @@ test("remarkMessageLinks: text inside inlineCode is left alone", () => {
|
||||
assert.equal(kids[0].type, "inlineCode");
|
||||
assert.equal(kids[0].value, "buzz://message?channel=c&id=m");
|
||||
});
|
||||
|
||||
test("remarkAgentConversationLinks: bare buzz://task URL is replaced", () => {
|
||||
const tree = runTaskPlugin(paragraph(text("buzz://task?channel=c&reply=r")));
|
||||
const para = tree.children[0];
|
||||
assert.equal(para.children.length, 1);
|
||||
assert.equal(para.children[0].type, "agent-conversation-link");
|
||||
assert.equal(para.children[0].value, "buzz://task?channel=c&reply=r");
|
||||
assert.equal(para.children[0].data.hName, "agent-conversation-link");
|
||||
});
|
||||
|
||||
test("remarkAgentConversationLinks: trailing punctuation stays outside URL", () => {
|
||||
const tree = runTaskPlugin(
|
||||
paragraph(text("see buzz://task?channel=c&reply=r.")),
|
||||
);
|
||||
const kids = tree.children[0].children;
|
||||
assert.equal(kids.length, 3);
|
||||
assert.equal(kids[0].value, "see ");
|
||||
assert.equal(kids[1].type, "agent-conversation-link");
|
||||
assert.equal(kids[1].value, "buzz://task?channel=c&reply=r");
|
||||
assert.equal(kids[2].value, ".");
|
||||
});
|
||||
|
||||
test("remarkAgentConversationLinks: non-task buzz:// URLs are not matched", () => {
|
||||
const original = "buzz://message?channel=c&id=m";
|
||||
const tree = runTaskPlugin(paragraph(text(original)));
|
||||
const kids = tree.children[0].children;
|
||||
assert.equal(kids.length, 1);
|
||||
assert.equal(kids[0].type, "text");
|
||||
assert.equal(kids[0].value, original);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
parseAgentConversationLink,
|
||||
type ParsedAgentConversationLink,
|
||||
} from "@/features/agents/agentConversationLink";
|
||||
import remarkAgentConversationLinks from "@/features/agents/remarkAgentConversationLinks";
|
||||
import {
|
||||
parseMessageLink,
|
||||
resolveMessageLinkRenderTarget,
|
||||
@@ -1941,6 +1942,37 @@ function createMarkdownComponents(
|
||||
/>
|
||||
);
|
||||
},
|
||||
"agent-conversation-link": ({
|
||||
children,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
}) => {
|
||||
const { onOpenAgentConversationLink } = runtimeRef.current;
|
||||
const href = String(children ?? "");
|
||||
const parsed = parseAgentConversationLink(href);
|
||||
if (!parsed.ok) {
|
||||
return <span>{href}</span>;
|
||||
}
|
||||
|
||||
if (!interactive) {
|
||||
return <span className="font-medium text-current">Open task</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-agent-conversation-link=""
|
||||
aria-label="Open task"
|
||||
title={href}
|
||||
className="cursor-pointer font-medium text-primary underline underline-offset-4 transition-colors hover:text-primary/80"
|
||||
onClick={() => {
|
||||
onOpenAgentConversationLink(parsed.value);
|
||||
}}
|
||||
>
|
||||
Open task
|
||||
</button>
|
||||
);
|
||||
},
|
||||
} as Components;
|
||||
}
|
||||
|
||||
@@ -2021,6 +2053,7 @@ function MarkdownInner({
|
||||
remarkBreaks,
|
||||
remarkSpoilers,
|
||||
remarkMessageLinks,
|
||||
remarkAgentConversationLinks,
|
||||
[remarkMentions, { mentionNames }],
|
||||
[remarkChannelLinks, { channelNames }],
|
||||
[remarkCustomEmoji, { customEmoji }],
|
||||
|
||||
Reference in New Issue
Block a user