diff --git a/app/(authenticated)/chat/[sessionId]/_components/conversation/index.test.tsx b/app/(authenticated)/chat/[sessionId]/_components/conversation/index.test.tsx
index 46388d23..5c025b32 100644
--- a/app/(authenticated)/chat/[sessionId]/_components/conversation/index.test.tsx
+++ b/app/(authenticated)/chat/[sessionId]/_components/conversation/index.test.tsx
@@ -4,8 +4,57 @@ import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { ChatConversation } from ".";
import type { ChatAgent } from "../chat-agent";
+import { sendMessageOutputSchema } from "@shared/chat/message-delivery";
describe("chat conversation", () => {
+ it.each([
+ "Please keep *literal stars* in the filename.",
+ "Save the `draft` filename.",
+ "#42 is my order number.",
+ "1. First note\n2. Second note",
+ "Line one \nLine two",
+ ])("renders delivered plain text exactly: %s", async (text) => {
+ const agent = await deliveredAgent(text);
+ const markup = renderToStaticMarkup(
+
+ );
+ expect(markup).toContain(text);
+ expect(markup).not.toMatch(/<(?:em|code|ol|h1)[ >]/u);
+ });
+
+ it.each([
+ "https://example.com/report",
+ "See https://example.com/report_2026?view=notes for details.",
+ ])("keeps delivered HTTP URLs tappable: %s", async (text) => {
+ const markup = renderToStaticMarkup(
+
+ );
+ expect(markup).toContain('data-streamdown="link"');
+ expect(markup).toContain("https://example.com/report");
+ expect(markup).toContain(
+ text.startsWith("See ") ? "for details." : "https://example.com/report"
+ );
+ });
+
+ it("keeps intentional trace Markdown", () => {
+ const agent = {
+ data: { messages: [message("turn-1:user", "*trace emphasis*")] },
+ error: undefined,
+ events: [],
+ respond: async () => undefined,
+ status: "ready",
+ } satisfies Pick<
+ ChatAgent,
+ "data" | "error" | "events" | "respond" | "status"
+ >;
+ expect(
+ renderToStaticMarkup()
+ ).toContain("trace emphasis");
+ });
+
it("shows send_message output instead of assistant stream text", () => {
const agent = {
data: {
@@ -109,6 +158,41 @@ describe("chat conversation", () => {
});
});
+async function deliveredAgent(text: string) {
+ const output = sendMessageOutputSchema.parse({ kind: "message", text });
+ return {
+ data: {
+ messages: [
+ { id: "turn-1:assistant", parts: [], role: "assistant" as const },
+ ],
+ },
+ error: undefined,
+ events: [
+ {
+ data: {
+ result: {
+ callId: "call-plain",
+ kind: "tool-result" as const,
+ output,
+ toolName: "send_message",
+ },
+ sequence: 0,
+ status: "completed" as const,
+ stepIndex: 0,
+ turnId: "turn-1",
+ },
+ meta: { at: "2026-10-07T00:00:00.000Z", id: "event-plain" },
+ type: "action.result" as const,
+ },
+ ],
+ respond: async () => undefined,
+ status: "ready" as const,
+ } satisfies Pick<
+ ChatAgent,
+ "data" | "error" | "events" | "respond" | "status"
+ >;
+}
+
function message(id: string, text: string): EveMessage {
return {
id,
diff --git a/app/(authenticated)/chat/[sessionId]/_components/conversation/message/parts.tsx b/app/(authenticated)/chat/[sessionId]/_components/conversation/message/parts.tsx
index 5fdd3d60..ba05e705 100644
--- a/app/(authenticated)/chat/[sessionId]/_components/conversation/message/parts.tsx
+++ b/app/(authenticated)/chat/[sessionId]/_components/conversation/message/parts.tsx
@@ -34,6 +34,24 @@ export function AgentMessagePart({
case "step-start":
return null;
case "text":
+ if (userVisibleOnly) {
+ return (
+
+ {part.text.split(/(https?:\/\/\S+)/iu).map((text, index) =>
+ /^https?:\/\//iu.test(text) ? (
+
+ {text}
+
+ ) : (
+ text
+ )
+ )}
+
+ );
+ }
return (
{part.text}
diff --git a/app/(authenticated)/chat/[sessionId]/_lib/message-events.test.ts b/app/(authenticated)/chat/[sessionId]/_lib/message-events.test.ts
index 50e0cfa5..ecc4f5d7 100644
--- a/app/(authenticated)/chat/[sessionId]/_lib/message-events.test.ts
+++ b/app/(authenticated)/chat/[sessionId]/_lib/message-events.test.ts
@@ -153,7 +153,7 @@ describe("iMessage event projection", () => {
expect(sentMessages(events).get("turn-1:assistant")).toEqual([
expect.objectContaining({
- parts: [expect.objectContaining({ text: "line one \nline two" })],
+ parts: [expect.objectContaining({ text: "line one\nline two" })],
}),
]);
});
diff --git a/app/(authenticated)/chat/[sessionId]/_lib/message-events.ts b/app/(authenticated)/chat/[sessionId]/_lib/message-events.ts
index 7497e8ea..022ea533 100644
--- a/app/(authenticated)/chat/[sessionId]/_lib/message-events.ts
+++ b/app/(authenticated)/chat/[sessionId]/_lib/message-events.ts
@@ -61,12 +61,7 @@ export function sentMessages(events: readonly MessageStreamEvent[]) {
});
} else if (delivery) {
const { output } = delivery;
- // Delivered text is plain and reaches the user verbatim. The chat view
- // renders text parts as Markdown, so keep every line break as a hard break.
- const text =
- output.kind === "link"
- ? output.url
- : output.text?.replaceAll("\n", " \n");
+ const text = output.kind === "link" ? output.url : output.text;
if (text) {
parts.push({
state: "done",
diff --git a/tests/agent/tools/share-contact.test.ts b/tests/agent/tools/share-contact.test.ts
index 8d3d0767..9f55bbd9 100644
--- a/tests/agent/tools/share-contact.test.ts
+++ b/tests/agent/tools/share-contact.test.ts
@@ -72,6 +72,25 @@ beforeEach(() => {
afterEach(() => vi.unstubAllGlobals());
describe("share_contact", () => {
+ it("preserves exact plain text in the send_message producer", async () => {
+ const text =
+ "Keep *literal stars*, `draft` and the spaces \nbefore this line.";
+ const resolve = messaging.events["turn.started"];
+ if (!resolve) throw new Error("Expected messaging resolver.");
+ const tools = await resolve({}, {
+ channel: { kind: "http", metadata: {} },
+ messages: [],
+ model: null,
+ session: toolContext().session,
+ } satisfies DynamicResolveContext);
+ if (!tools || !("send_message" in tools))
+ throw new Error("Expected send_message.");
+ const input = sendMessageOutputSchema.parse({ kind: "message", text });
+ expect(await tools.send_message.execute(input, toolContext())).toEqual(
+ input
+ );
+ });
+
it("renders the contact in browser chat and suppresses repeats without sending to Linq", async () => {
const tool = await shareContactTool();
const first = await tool.execute(