Agents
diff --git a/desktop/src/apps/MessagesApp.tsx b/desktop/src/apps/MessagesApp.tsx
index dfee4b4ee..837c68705 100644
--- a/desktop/src/apps/MessagesApp.tsx
+++ b/desktop/src/apps/MessagesApp.tsx
@@ -67,6 +67,7 @@ import {
import { displayAuthor } from "./chat/format-author";
import { useProcessStore } from "@/stores/process-store";
import { getApp } from "@/registry/app-registry";
+import { CodeBlock } from "@/components/CodeBlock";
/* ------------------------------------------------------------------ */
/* Types */
@@ -212,6 +213,29 @@ function relativeTime(ts: number | string): string {
}
function renderContent(text: string) {
+ // Split on fenced code blocks first, then apply inline markdown to non-code segments.
+ const result: (string | React.ReactElement)[] = [];
+ const fenceRegex = /```(?:[^\n]*)?\n([\s\S]*?)```/g;
+ let lastFence = 0;
+ let fenceMatch: RegExpExecArray | null;
+ let seg = 0;
+
+ // Each segment gets a distinct key prefix so keys can never collide no
+ // matter how many inline elements one segment produces.
+ while ((fenceMatch = fenceRegex.exec(text)) !== null) {
+ if (fenceMatch.index > lastFence) {
+ result.push(...renderInline(text.slice(lastFence, fenceMatch.index), `s${seg++}`));
+ }
+ result.push(
);
+ lastFence = fenceMatch.index + fenceMatch[0].length;
+ }
+ if (lastFence < text.length) {
+ result.push(...renderInline(text.slice(lastFence), `s${seg}`));
+ }
+ return result;
+}
+
+function renderInline(text: string, keyPrefix: string) {
// basic markdown: bold, italic, inline code
const parts: (string | React.ReactElement)[] = [];
const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g;
@@ -220,9 +244,9 @@ function renderContent(text: string) {
let key = 0;
while ((match = regex.exec(text)) !== null) {
if (match.index > last) parts.push(text.slice(last, match.index));
- if (match[2]) parts.push(
{match[2]});
- else if (match[3]) parts.push(
{match[3]});
- else if (match[4]) parts.push(
{match[4]});
+ if (match[2]) parts.push(
{match[2]});
+ else if (match[3]) parts.push(
{match[3]});
+ else if (match[4]) parts.push(
{match[4]});
last = match.index + match[0].length;
}
if (last < text.length) parts.push(text.slice(last));
@@ -1032,6 +1056,15 @@ export function MessagesApp({
} catch { /* ignore */ }
};
+ const handleCopyText = async (msgId: string) => {
+ setOverflowMenu(null);
+ const msg = messages.find((m) => m.id === msgId);
+ if (!msg) return;
+ try {
+ await navigator.clipboard.writeText(msg.content);
+ } catch { /* ignore */ }
+ };
+
const handlePin = async (msg: Message) => {
setOverflowMenu(null);
const isPinned = pinnedMessages.some((p) => p.id === msg.id);
@@ -2081,6 +2114,7 @@ export function MessagesApp({
onEdit={() => handleEdit(msg.id)}
onDelete={() => handleDelete(msg.id)}
onCopyLink={() => handleCopyLink(msg.id)}
+ onCopyText={() => handleCopyText(msg.id)}
onPin={() => handlePin(msg)}
onMarkUnread={() => handleMarkUnread(msg.id)}
onClose={() => setOverflowMenu(null)}
diff --git a/desktop/src/apps/chat/MessageOverflowMenu.tsx b/desktop/src/apps/chat/MessageOverflowMenu.tsx
index 13d9e5753..e8b90af93 100644
--- a/desktop/src/apps/chat/MessageOverflowMenu.tsx
+++ b/desktop/src/apps/chat/MessageOverflowMenu.tsx
@@ -7,6 +7,7 @@ export interface MessageOverflowMenuProps {
onEdit: () => void;
onDelete: () => void;
onCopyLink: () => void;
+ onCopyText?: () => void;
onPin: () => void;
onMarkUnread: () => void;
onClose?: () => void;
@@ -14,7 +15,7 @@ export interface MessageOverflowMenuProps {
export function MessageOverflowMenu({
isOwn, isHuman, isPinned = false,
- onEdit, onDelete, onCopyLink, onPin, onMarkUnread, onClose,
+ onEdit, onDelete, onCopyLink, onCopyText, onPin, onMarkUnread, onClose,
}: MessageOverflowMenuProps) {
const containerRef = useRef
(null);
@@ -66,6 +67,8 @@ export function MessageOverflowMenu({
)}
+ {onCopyText && }
{isHuman && (