feat(webui): add lightweight session messaging via mentions

This commit is contained in:
chengyongru
2026-08-19 01:15:56 +08:00
committed by chengyongru
parent 2bdb11eeba
commit 0e184965e8
76 changed files with 8297 additions and 658 deletions
+93
View File
@@ -20,6 +20,7 @@ import {
import { useTranslation } from "react-i18next";
import { AttachmentTile } from "@/components/AttachmentTile";
import { sessionHandleColor } from "@/components/CliAppMentionText";
import { ImageLightbox } from "@/components/ImageLightbox";
import { MarkdownText } from "@/components/MarkdownText";
import { SlashCommandText } from "@/components/SlashCommandText";
@@ -48,6 +49,7 @@ import type {
UIMessage,
MessageDeliveryErrorKind,
MessageDeliveryStatus,
SessionHandle,
} from "@/lib/types";
interface MessageBubbleProps {
@@ -61,6 +63,7 @@ interface MessageBubbleProps {
cliApps?: CliAppInfo[];
mcpPresets?: McpPresetInfo[];
slashCommands?: SlashCommand[];
sessionDirectory?: SessionHandle[];
onOpenFilePreview?: (path: string) => void;
onForkFromHere?: () => void;
}
@@ -259,6 +262,77 @@ function UserDeliveryStatus({
);
}
function IncomingSessionMessage({
message,
showCopyAction,
sessionDirectory,
onOpenFilePreview,
}: {
message: UIMessage;
showCopyAction: boolean;
sessionDirectory: SessionHandle[];
onOpenFilePreview?: (path: string) => void;
}) {
const handle = message.sessionMessage!.session;
const activeSession = sessionDirectory.find((candidate) => candidate.id === handle.id);
const color = sessionHandleColor(handle.color_slot);
const createdAtLabel = formatMessageEndTime(message.createdAt);
const handleName = `@${handle.name}`;
const name = <span className="font-medium text-foreground">{handleName}</span>;
return (
<div
data-handle-message="incoming"
className="group w-full text-[15px]"
style={{ lineHeight: "var(--cjk-line-height)" }}
>
<div
data-handle-message-body
className="min-w-0 rounded-es-[16px] border-s-2 bg-background pb-1 ps-2.5"
style={{ borderInlineStartColor: color }}
>
<div className="mb-1.5 flex items-center text-[12px] text-muted-foreground">
{activeSession?.session_key ? (
<a
href={`#/chat/${encodeURIComponent(activeSession.session_key)}`}
className="rounded-sm underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
>
{name}
</a>
) : name}
</div>
<div data-assistant-selectable="true" className="min-w-0">
<MarkdownText
preserveStreamingLayout
onOpenFilePreview={onOpenFilePreview}
sessionHandles={sessionDirectory}
>
{message.content}
</MarkdownText>
</div>
</div>
{createdAtLabel || showCopyAction ? (
<TooltipProvider delayDuration={220} skipDelayDuration={80}>
<div
data-handle-footer
className="mt-1 flex min-h-8 items-center gap-1.5 text-muted-foreground"
>
{showCopyAction ? <MessageCopyButton content={message.content} /> : null}
{createdAtLabel ? (
<MessageTimestamp
timestamp={message.createdAt}
tooltipLabel={fmtDateTime(message.createdAt)}
>
{createdAtLabel}
</MessageTimestamp>
) : null}
</div>
</TooltipProvider>
) : null}
</div>
);
}
/** Render user turns as compact bubbles and assistant turns as document-like prose. */
export function MessageBubble({
message,
@@ -268,6 +342,7 @@ export function MessageBubble({
cliApps = [],
mcpPresets = [],
slashCommands = [],
sessionDirectory = [],
onOpenFilePreview,
onForkFromHere,
}: MessageBubbleProps) {
@@ -285,6 +360,17 @@ export function MessageBubble({
return <TraceGroup message={message} />;
}
if (message.role === "user" && message.sessionMessage?.direction === "incoming") {
return (
<IncomingSessionMessage
message={message}
showCopyAction={showCopyAction}
sessionDirectory={sessionDirectory}
onOpenFilePreview={onOpenFilePreview}
/>
);
}
if (message.role === "user") {
const images = message.images ?? [];
const media = message.media ?? [];
@@ -308,6 +394,9 @@ export function MessageBubble({
cliApps={mentionCliApps}
mcpPresets={mentionMcpPresets}
sessionMentions={message.sessionMentions}
sessionHandles={message.sessionHandles}
attachedCliApps={message.cliApps}
attachedMcpPresets={message.mcpPresets}
/>
</>
) : (
@@ -316,6 +405,9 @@ export function MessageBubble({
cliApps={mentionCliApps}
mcpPresets={mentionMcpPresets}
sessionMentions={message.sessionMentions}
sessionHandles={message.sessionHandles}
attachedCliApps={message.cliApps}
attachedMcpPresets={message.mcpPresets}
/>
);
return (
@@ -433,6 +525,7 @@ export function MessageBubble({
streaming={!!message.isStreaming}
preserveStreamingLayout
onOpenFilePreview={onOpenFilePreview}
sessionHandles={sessionDirectory}
>
{message.content}
</MarkdownText>