refactor: simplify cross-session messaging
This commit is contained in:
@@ -50,7 +50,7 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { MAX_WORKBENCH_PANES } from "@/components/workbench/workbench-model";
|
||||
import { SIDEBAR_SELECTION_ITEM_CLASS } from "@/components/SidebarSelectionHighlight";
|
||||
import { SessionHandleHighlight, sessionHandleColor } from "@/components/CliAppMentionText";
|
||||
import { SessionHandleLabel } from "@/components/SessionHandleLabel";
|
||||
import { deriveTitle, relativeTime, visibleSessionPreview } from "@/lib/format";
|
||||
import {
|
||||
COLLAPSED_CHATS_VISIBLE_COUNT,
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
type ChatGroupLabels,
|
||||
} from "@/lib/chat-groups";
|
||||
import { deriveTemporaryChatTitle } from "@/lib/temporary-chat";
|
||||
import { sessionHandleColor } from "@/lib/session-handle";
|
||||
import {
|
||||
clearDraggedSession,
|
||||
hasDraggedSession,
|
||||
@@ -120,7 +121,7 @@ function SidebarSelectionTrack({
|
||||
active ? "scale-x-100" : "scale-x-0",
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: handle ? sessionHandleColor(handle.color_slot) : "currentColor",
|
||||
backgroundColor: handle ? sessionHandleColor(handle.id) : "currentColor",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -130,12 +131,11 @@ function SidebarSessionHandle({ handle }: { handle: ChatSummary["handle"] }) {
|
||||
if (!handle) return null;
|
||||
return (
|
||||
<span
|
||||
data-sidebar-handle-handle
|
||||
className="flex max-w-20 shrink-0 items-center overflow-hidden whitespace-nowrap text-[11px] font-medium leading-5"
|
||||
>
|
||||
<SessionHandleHighlight handle={handle}>
|
||||
<SessionHandleLabel id={handle.id}>
|
||||
@{handle.name}
|
||||
</SessionHandleHighlight>
|
||||
</SessionHandleLabel>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
@@ -7,12 +7,8 @@ import {
|
||||
} from "@/components/InlineTokenHighlight";
|
||||
import { useLogoFallback } from "@/hooks/useLogoFallback";
|
||||
import { logoFallbackUrls } from "@/lib/provider-brand";
|
||||
import type {
|
||||
CliAppInfo,
|
||||
McpPresetInfo,
|
||||
SessionHandle,
|
||||
SessionMention,
|
||||
} from "@/lib/types";
|
||||
import { sessionHandleColor } from "@/lib/session-handle";
|
||||
import type { CliAppInfo, McpPresetInfo, SessionMention } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type CliAppMentionSegment =
|
||||
@@ -22,56 +18,8 @@ type CliAppMentionSegment =
|
||||
export type CapabilityMentionSegment =
|
||||
| CliAppMentionSegment
|
||||
| { kind: "mcp"; text: string; preset: McpPresetInfo }
|
||||
| { kind: "handle"; text: string; handle: SessionHandle };
|
||||
|
||||
export type SessionReferenceSegment =
|
||||
| { kind: "text"; text: string }
|
||||
| { kind: "session"; text: string; mention: SessionMention };
|
||||
|
||||
export interface TokenSelection<T> {
|
||||
mention: T;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export type SessionHandleSelection = TokenSelection<SessionHandle>;
|
||||
export type SessionMentionSelection = TokenSelection<SessionMention>;
|
||||
|
||||
const SESSION_HANDLE_COLOR_COUNT = 8;
|
||||
|
||||
export function sessionHandleColor(colorSlot: number): string {
|
||||
const slot = Number.isFinite(colorSlot)
|
||||
? Math.abs(Math.trunc(colorSlot)) % SESSION_HANDLE_COLOR_COUNT
|
||||
: 0;
|
||||
return `var(--session-handle-${slot})`;
|
||||
}
|
||||
|
||||
export function SessionHandleHighlight({
|
||||
handle,
|
||||
children,
|
||||
className,
|
||||
testId,
|
||||
}: {
|
||||
handle: Pick<SessionHandle, "color_slot" | "name">;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className="inline border-b-2"
|
||||
style={{ borderBottomColor: sessionHandleColor(handle.color_slot) }}
|
||||
>
|
||||
<InlineTokenHighlight
|
||||
testId={testId}
|
||||
className={cn("text-foreground", className)}
|
||||
>
|
||||
{children}
|
||||
</InlineTokenHighlight>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function cliAppInitials(app: CliAppInfo): string {
|
||||
const value = app.display_name || app.name;
|
||||
return (
|
||||
@@ -83,7 +31,6 @@ export function cliAppInitials(app: CliAppInfo): string {
|
||||
.join("") || app.name.slice(0, 2).toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function mcpPresetInitials(preset: Pick<McpPresetInfo, "name" | "display_name">): string {
|
||||
const value = preset.display_name || preset.name;
|
||||
return (
|
||||
@@ -95,15 +42,13 @@ export function mcpPresetInitials(preset: Pick<McpPresetInfo, "name" | "display_
|
||||
.join("") || preset.name.slice(0, 2).toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function splitCapabilityMentionSegments(
|
||||
value: string,
|
||||
cliApps: CliAppInfo[],
|
||||
mcpPresets: McpPresetInfo[] = [],
|
||||
sessionHandles: SessionHandle[] = [],
|
||||
handleSelections?: SessionHandleSelection[],
|
||||
sessionMentions: SessionMention[] = [],
|
||||
): CapabilityMentionSegment[] {
|
||||
if (!value || (cliApps.length === 0 && mcpPresets.length === 0 && sessionHandles.length === 0)) {
|
||||
if (!value || (cliApps.length === 0 && mcpPresets.length === 0 && sessionMentions.length === 0)) {
|
||||
return value ? [{ kind: "text", text: value }] : [];
|
||||
}
|
||||
const cliAppsByName = new Map(
|
||||
@@ -116,13 +61,10 @@ export function splitCapabilityMentionSegments(
|
||||
.filter((preset) => preset.installed && preset.configured)
|
||||
.map((preset) => [preset.name.toLowerCase(), preset]),
|
||||
);
|
||||
const handlesByName = new Map(
|
||||
sessionHandles.map((handle) => [handle.name.toLowerCase(), handle]),
|
||||
const sessionsByName = new Map(
|
||||
sessionMentions.map((mention) => [mention.name.toLowerCase(), mention]),
|
||||
);
|
||||
const selectedSessionNames = new Set(
|
||||
(handleSelections ?? []).map((selection) => selection.mention.name.toLowerCase()),
|
||||
);
|
||||
if (cliAppsByName.size === 0 && mcpPresetsByName.size === 0 && handlesByName.size === 0) {
|
||||
if (cliAppsByName.size === 0 && mcpPresetsByName.size === 0 && sessionsByName.size === 0) {
|
||||
return [{ kind: "text", text: value }];
|
||||
}
|
||||
|
||||
@@ -134,15 +76,13 @@ export function splitCapabilityMentionSegments(
|
||||
const prefix = match[1] ?? "";
|
||||
const name = match[2] ?? "";
|
||||
const key = name.toLowerCase();
|
||||
const session = sessionsByName.get(key);
|
||||
const app = session ? null : cliAppsByName.get(key);
|
||||
const preset = session || app ? null : mcpPresetsByName.get(key);
|
||||
if (!app && !preset && !session) continue;
|
||||
|
||||
const mentionStart = match.index + prefix.length;
|
||||
const mentionEnd = mentionStart + name.length + 1;
|
||||
const handle = handleSelections
|
||||
? selectedSessionNames.has(key) ? handlesByName.get(key) : undefined
|
||||
: handlesByName.get(key);
|
||||
const app = handle ? null : cliAppsByName.get(key);
|
||||
const preset = handle || app ? null : mcpPresetsByName.get(key);
|
||||
if (!app && !preset && !handle) continue;
|
||||
|
||||
if (mentionStart > cursor) {
|
||||
segments.push({ kind: "text", text: value.slice(cursor, mentionStart) });
|
||||
}
|
||||
@@ -150,51 +90,18 @@ export function splitCapabilityMentionSegments(
|
||||
segments.push({ kind: "cli", text: value.slice(mentionStart, mentionEnd), app });
|
||||
} else if (preset) {
|
||||
segments.push({ kind: "mcp", text: value.slice(mentionStart, mentionEnd), preset });
|
||||
} else if (handle) {
|
||||
segments.push({ kind: "handle", text: value.slice(mentionStart, mentionEnd), handle });
|
||||
} else if (session) {
|
||||
segments.push({
|
||||
kind: "session",
|
||||
text: value.slice(mentionStart, mentionEnd),
|
||||
mention: session,
|
||||
});
|
||||
}
|
||||
cursor = mentionEnd;
|
||||
}
|
||||
if (cursor < value.length) segments.push({ kind: "text", text: value.slice(cursor) });
|
||||
return segments.length ? segments : [{ kind: "text", text: value }];
|
||||
}
|
||||
|
||||
export function splitSessionReferenceSegments(
|
||||
value: string,
|
||||
sessionMentions: SessionMention[] = [],
|
||||
sessionSelections?: SessionMentionSelection[],
|
||||
allowLegacyAt = false,
|
||||
): SessionReferenceSegment[] {
|
||||
if (!value || sessionMentions.length === 0) return value ? [{ kind: "text", text: value }] : [];
|
||||
const sessionsByName = new Map(
|
||||
sessionMentions.map((mention) => [mention.name.toLowerCase(), mention]),
|
||||
);
|
||||
const selectedSessionByStart = new Map(
|
||||
(sessionSelections ?? []).map((selection) => [selection.start, selection]),
|
||||
);
|
||||
const segments: SessionReferenceSegment[] = [];
|
||||
const referenceRe = allowLegacyAt
|
||||
? /(^|[\s([{])([#@])([\p{L}\p{N}_-]+)(?=$|[^\p{L}\p{N}_-])/giu
|
||||
: /(^|[\s([{])(#)([\p{L}\p{N}_-]+)(?=$|[^\p{L}\p{N}_-])/giu;
|
||||
let cursor = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = referenceRe.exec(value)) !== null) {
|
||||
const prefix = match[1] ?? "";
|
||||
const name = match[3] ?? "";
|
||||
const start = match.index + prefix.length;
|
||||
const end = start + name.length + 1;
|
||||
const selected = selectedSessionByStart.get(start);
|
||||
const mention = sessionSelections
|
||||
? selected?.end === end && selected.mention.name.toLowerCase() === name.toLowerCase()
|
||||
? selected.mention
|
||||
: undefined
|
||||
: sessionsByName.get(name.toLowerCase());
|
||||
if (!mention) continue;
|
||||
if (start > cursor) segments.push({ kind: "text", text: value.slice(cursor, start) });
|
||||
segments.push({ kind: "session", text: value.slice(start, end), mention });
|
||||
cursor = end;
|
||||
if (cursor < value.length) {
|
||||
segments.push({ kind: "text", text: value.slice(cursor) });
|
||||
}
|
||||
if (cursor < value.length) segments.push({ kind: "text", text: value.slice(cursor) });
|
||||
return segments.length ? segments : [{ kind: "text", text: value }];
|
||||
}
|
||||
|
||||
@@ -227,42 +134,10 @@ export function CapabilityMentionToken({
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <SessionHandleToken handle={segment.handle} label={segment.text} variant={variant} />;
|
||||
return <SessionMentionToken mention={segment.mention} label={segment.text} variant={variant} />;
|
||||
}
|
||||
|
||||
export function SessionHandleToken({
|
||||
handle,
|
||||
label,
|
||||
variant,
|
||||
}: {
|
||||
handle: SessionHandle;
|
||||
label: string;
|
||||
variant: "composer" | "message";
|
||||
}) {
|
||||
const testIdPrefix = variant === "composer" ? "composer" : "message";
|
||||
const color = sessionHandleColor(handle.color_slot);
|
||||
const token = (
|
||||
<SessionHandleHighlight
|
||||
handle={handle}
|
||||
testId={`${testIdPrefix}-handle-mention-${handle.name}`}
|
||||
className={variant === "composer" ? "font-normal" : undefined}
|
||||
>
|
||||
{label}
|
||||
</SessionHandleHighlight>
|
||||
);
|
||||
if (variant === "composer" || !handle.session_key) return token;
|
||||
return (
|
||||
<a
|
||||
href={`#/chat/${encodeURIComponent(handle.session_key)}`}
|
||||
className="rounded-sm underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
||||
style={{ textDecorationColor: color }}
|
||||
>
|
||||
{token}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function SessionReferenceToken({
|
||||
export function SessionMentionToken({
|
||||
mention,
|
||||
label,
|
||||
variant,
|
||||
@@ -272,11 +147,14 @@ export function SessionReferenceToken({
|
||||
variant: "composer" | "message";
|
||||
}) {
|
||||
const testIdPrefix = variant === "composer" ? "composer" : "message";
|
||||
const color = mention.id
|
||||
? sessionHandleColor(mention.id)
|
||||
: INLINE_TOKEN_HIGHLIGHT_COLOR;
|
||||
const token = (
|
||||
<InlineTokenHighlight
|
||||
testId={`${testIdPrefix}-session-reference-${mention.name}`}
|
||||
testId={`${testIdPrefix}-session-mention-${mention.name}`}
|
||||
title={`Session: ${mention.title || mention.name}`}
|
||||
color={INLINE_TOKEN_HIGHLIGHT_COLOR}
|
||||
color={color}
|
||||
className={variant === "composer" ? "font-normal" : undefined}
|
||||
>
|
||||
{label}
|
||||
@@ -287,7 +165,7 @@ export function SessionReferenceToken({
|
||||
<a
|
||||
href={`#/chat/${encodeURIComponent(mention.session_key)}`}
|
||||
className="rounded-sm underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
||||
style={{ textDecorationColor: INLINE_TOKEN_HIGHLIGHT_COLOR }}
|
||||
style={{ textDecorationColor: color }}
|
||||
>
|
||||
{token}
|
||||
</a>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function InlineTokenHighlight({
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: string;
|
||||
color: string;
|
||||
testId?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
@@ -25,7 +25,7 @@ export function InlineTokenHighlight({
|
||||
"relative inline font-[550] transition-colors duration-150",
|
||||
className,
|
||||
)}
|
||||
style={color ? { color } : undefined}
|
||||
style={{ color }}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SessionHandle } from "@/lib/types";
|
||||
|
||||
interface MarkdownTextProps {
|
||||
children: string;
|
||||
@@ -16,7 +15,6 @@ interface MarkdownTextProps {
|
||||
streaming?: boolean;
|
||||
preserveStreamingLayout?: boolean;
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
sessionHandles?: SessionHandle[];
|
||||
}
|
||||
|
||||
const loadMarkdownRenderer = () => import("@/components/MarkdownTextRenderer");
|
||||
@@ -28,14 +26,12 @@ const MemoizedMarkdownRenderer = memo(function MemoizedMarkdownRenderer({
|
||||
highlightCode,
|
||||
streaming,
|
||||
onOpenFilePreview,
|
||||
sessionHandles,
|
||||
}: {
|
||||
source: string;
|
||||
className?: string;
|
||||
highlightCode: boolean;
|
||||
streaming: boolean;
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
sessionHandles?: SessionHandle[];
|
||||
}) {
|
||||
return (
|
||||
<LazyMarkdownRenderer
|
||||
@@ -43,7 +39,6 @@ const MemoizedMarkdownRenderer = memo(function MemoizedMarkdownRenderer({
|
||||
highlightCode={highlightCode}
|
||||
streaming={streaming}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
sessionHandles={sessionHandles}
|
||||
>
|
||||
{source}
|
||||
</LazyMarkdownRenderer>
|
||||
@@ -82,7 +77,6 @@ export function MarkdownText({
|
||||
streaming = false,
|
||||
preserveStreamingLayout = false,
|
||||
onOpenFilePreview,
|
||||
sessionHandles,
|
||||
}: MarkdownTextProps) {
|
||||
const renderedSource = children;
|
||||
const renderPhase = streaming ? "streaming" : "complete";
|
||||
@@ -114,7 +108,6 @@ export function MarkdownText({
|
||||
highlightCode={highlightCode}
|
||||
streaming={renderWithStreamingLayout}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
sessionHandles={sessionHandles}
|
||||
/>
|
||||
</Suspense>
|
||||
</MarkdownRendererBoundary>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Streamdown, type Components, type StreamdownProps } from "streamdown";
|
||||
|
||||
import { AttachmentTile } from "@/components/AttachmentTile";
|
||||
import { CodeBlock } from "@/components/CodeBlock";
|
||||
import { SessionHandleHighlight, sessionHandleColor } from "@/components/CliAppMentionText";
|
||||
import {
|
||||
INLINE_TOKEN_HIGHLIGHT_COLOR,
|
||||
InlineTokenHighlight,
|
||||
@@ -35,7 +34,6 @@ import { inferMediaKind } from "@/lib/media";
|
||||
import { browserSafeFaviconUrls } from "@/lib/provider-brand";
|
||||
import { remarkTexMath } from "@/lib/remark-tex-math";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SessionHandle } from "@/lib/types";
|
||||
|
||||
import "katex/dist/katex.min.css";
|
||||
import "streamdown/styles.css";
|
||||
@@ -46,13 +44,11 @@ interface MarkdownTextRendererProps {
|
||||
highlightCode?: boolean;
|
||||
streaming?: boolean;
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
sessionHandles?: SessionHandle[];
|
||||
}
|
||||
|
||||
type MarkdownAstNode = {
|
||||
type: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
children?: MarkdownAstNode[];
|
||||
data?: {
|
||||
hName?: string;
|
||||
@@ -281,108 +277,7 @@ function remarkCjkStrongBoundaries() {
|
||||
};
|
||||
}
|
||||
|
||||
const SESSION_HANDLE_PATTERN = /@([\p{L}\p{N}_-]+)/gu;
|
||||
const SESSION_HANDLE_SKIP_NODES = new Set([
|
||||
"code",
|
||||
"html",
|
||||
"inlineCode",
|
||||
"inlineMath",
|
||||
"link",
|
||||
"linkReference",
|
||||
"math",
|
||||
]);
|
||||
const VOID_HTML_TAGS = new Set([
|
||||
"area",
|
||||
"base",
|
||||
"br",
|
||||
"col",
|
||||
"embed",
|
||||
"hr",
|
||||
"img",
|
||||
"input",
|
||||
"link",
|
||||
"meta",
|
||||
"param",
|
||||
"source",
|
||||
"track",
|
||||
"wbr",
|
||||
]);
|
||||
const RAW_HTML_TAG_PATTERN = /<\s*(\/?)\s*([a-z][\w:-]*)(?:\s[^<>]*?)?(\/?)\s*>/giu;
|
||||
|
||||
function normalizeSessionHandle(value: string): string {
|
||||
return value.normalize("NFKC").toLocaleLowerCase();
|
||||
}
|
||||
|
||||
function sessionHandleNodes(
|
||||
value: string,
|
||||
handlesByName: ReadonlyMap<string, SessionHandle>,
|
||||
): MarkdownAstNode[] | null {
|
||||
const replacement: MarkdownAstNode[] = [];
|
||||
let cursor = 0;
|
||||
for (const match of value.matchAll(SESSION_HANDLE_PATTERN)) {
|
||||
const start = match.index;
|
||||
const previous = start > 0 ? value[start - 1] : "";
|
||||
if (previous && /[\p{L}\p{N}_@-]/u.test(previous)) continue;
|
||||
const handle = handlesByName.get(normalizeSessionHandle(match[1]));
|
||||
if (!handle) continue;
|
||||
if (start > cursor) replacement.push(safeText(value.slice(cursor, start)));
|
||||
replacement.push({
|
||||
type: "link",
|
||||
url: `#session-handle/${encodeURIComponent(handle.session_key)}`,
|
||||
children: [safeText(match[0])],
|
||||
});
|
||||
cursor = start + match[0].length;
|
||||
}
|
||||
if (cursor === 0) return null;
|
||||
if (cursor < value.length) replacement.push(safeText(value.slice(cursor)));
|
||||
return replacement;
|
||||
}
|
||||
|
||||
function rawHtmlNestingDelta(value: string | undefined): number {
|
||||
if (!value) return 0;
|
||||
let delta = 0;
|
||||
for (const match of value.matchAll(RAW_HTML_TAG_PATTERN)) {
|
||||
const closing = match[1] === "/";
|
||||
const tagName = match[2].toLowerCase();
|
||||
const selfClosing = match[3] === "/" || VOID_HTML_TAGS.has(tagName);
|
||||
if (closing) delta -= 1;
|
||||
else if (!selfClosing) delta += 1;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
function transformKnownSessionHandles(
|
||||
node: MarkdownAstNode,
|
||||
handlesByName: ReadonlyMap<string, SessionHandle>,
|
||||
): void {
|
||||
if (
|
||||
!node.children
|
||||
|| SESSION_HANDLE_SKIP_NODES.has(node.type)
|
||||
|| node.type.startsWith("nanobotSafeHtml")
|
||||
) return;
|
||||
let rawHtmlDepth = 0;
|
||||
node.children = node.children.flatMap((child) => {
|
||||
if (child.type === "html") {
|
||||
rawHtmlDepth = Math.max(0, rawHtmlDepth + rawHtmlNestingDelta(child.value));
|
||||
return [child];
|
||||
}
|
||||
if (rawHtmlDepth > 0) return [child];
|
||||
if (child.type !== "text" || !child.value?.includes("@")) {
|
||||
transformKnownSessionHandles(child, handlesByName);
|
||||
return [child];
|
||||
}
|
||||
return sessionHandleNodes(child.value, handlesByName) ?? [child];
|
||||
});
|
||||
}
|
||||
|
||||
function remarkKnownSessionHandles({ handles }: { handles: SessionHandle[] }) {
|
||||
const handlesByName = new Map(
|
||||
handles.map((handle) => [normalizeSessionHandle(handle.name), handle]),
|
||||
);
|
||||
return (tree: MarkdownAstNode) => transformKnownSessionHandles(tree, handlesByName);
|
||||
}
|
||||
|
||||
const baseRemarkPlugins: NonNullable<StreamdownProps["remarkPlugins"]> = [
|
||||
const remarkPlugins: NonNullable<StreamdownProps["remarkPlugins"]> = [
|
||||
remarkBreaks,
|
||||
remarkGfm,
|
||||
[remarkMath, { singleDollarTextMath: false }],
|
||||
@@ -622,22 +517,8 @@ export default function MarkdownTextRenderer({
|
||||
highlightCode = true,
|
||||
streaming = false,
|
||||
onOpenFilePreview,
|
||||
sessionHandles = [],
|
||||
}: MarkdownTextRendererProps) {
|
||||
const { t } = useTranslation();
|
||||
const handlesBySessionKey = useMemo(
|
||||
() => new Map(sessionHandles.map((handle) => [handle.session_key, handle])),
|
||||
[sessionHandles],
|
||||
);
|
||||
const remarkPlugins = useMemo(
|
||||
() => sessionHandles.length > 0
|
||||
? [
|
||||
...baseRemarkPlugins,
|
||||
[remarkKnownSessionHandles, { handles: sessionHandles }],
|
||||
] as NonNullable<StreamdownProps["remarkPlugins"]>
|
||||
: baseRemarkPlugins,
|
||||
[sessionHandles],
|
||||
);
|
||||
const components = useMemo<Components>(
|
||||
() => ({
|
||||
code({ className: cls, children: kids, node: _node, ...props }) {
|
||||
@@ -731,30 +612,6 @@ export default function MarkdownTextRenderer({
|
||||
if (href === "streamdown:incomplete-link") {
|
||||
return <>{markdownChildren}</>;
|
||||
}
|
||||
if (href.startsWith("#session-handle/")) {
|
||||
let handle: SessionHandle | undefined;
|
||||
try {
|
||||
handle = handlesBySessionKey.get(decodeURIComponent(href.slice("#session-handle/".length)));
|
||||
} catch {
|
||||
handle = undefined;
|
||||
}
|
||||
if (!handle) return <>{markdownChildren}</>;
|
||||
const color = sessionHandleColor(handle.color_slot);
|
||||
return (
|
||||
<a
|
||||
href={`#/chat/${encodeURIComponent(handle.session_key)}`}
|
||||
className="rounded-sm no-underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
||||
style={{ textDecorationColor: color }}
|
||||
>
|
||||
<SessionHandleHighlight
|
||||
handle={handle}
|
||||
testId={`message-handle-mention-${handle.name}`}
|
||||
>
|
||||
{markdownChildren}
|
||||
</SessionHandleHighlight>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const sessionHref = sessionReferenceHref(href);
|
||||
if (sessionHref) {
|
||||
return (
|
||||
@@ -933,7 +790,7 @@ export default function MarkdownTextRenderer({
|
||||
);
|
||||
},
|
||||
}),
|
||||
[highlightCode, onOpenFilePreview, handlesBySessionKey, t],
|
||||
[highlightCode, onOpenFilePreview, t],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AttachmentTile } from "@/components/AttachmentTile";
|
||||
import { sessionHandleColor } from "@/components/CliAppMentionText";
|
||||
import { SessionHandleLabel } from "@/components/SessionHandleLabel";
|
||||
import { ImageLightbox } from "@/components/ImageLightbox";
|
||||
import { MarkdownText } from "@/components/MarkdownText";
|
||||
import { SlashCommandText } from "@/components/SlashCommandText";
|
||||
@@ -37,6 +37,7 @@ import { copyTextToClipboard } from "@/lib/clipboard";
|
||||
import { fmtDateTime, formatMessageEndTime } from "@/lib/format";
|
||||
import { toMediaAttachment } from "@/lib/media";
|
||||
import { matchingSlashCommand } from "@/lib/slash-command";
|
||||
import { sessionHandleColor } from "@/lib/session-handle";
|
||||
import { parseQuotedUserMessage } from "@/lib/user-message-quote";
|
||||
import type {
|
||||
CliAppInfo,
|
||||
@@ -49,7 +50,6 @@ import type {
|
||||
UIMessage,
|
||||
MessageDeliveryErrorKind,
|
||||
MessageDeliveryStatus,
|
||||
SessionHandle,
|
||||
} from "@/lib/types";
|
||||
|
||||
interface MessageBubbleProps {
|
||||
@@ -63,7 +63,6 @@ interface MessageBubbleProps {
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
slashCommands?: SlashCommand[];
|
||||
sessionDirectory?: SessionHandle[];
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
onForkFromHere?: () => void;
|
||||
}
|
||||
@@ -265,47 +264,34 @@ 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 color = sessionHandleColor(handle.id);
|
||||
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"
|
||||
data-session-message
|
||||
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}
|
||||
<SessionHandleLabel id={handle.id}>{handleName}</SessionHandleLabel>
|
||||
</div>
|
||||
<div data-assistant-selectable="true" className="min-w-0">
|
||||
<MarkdownText
|
||||
preserveStreamingLayout
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
sessionHandles={sessionDirectory}
|
||||
>
|
||||
{message.content}
|
||||
</MarkdownText>
|
||||
@@ -314,7 +300,6 @@ function IncomingSessionMessage({
|
||||
{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}
|
||||
@@ -342,7 +327,6 @@ export function MessageBubble({
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
slashCommands = [],
|
||||
sessionDirectory = [],
|
||||
onOpenFilePreview,
|
||||
onForkFromHere,
|
||||
}: MessageBubbleProps) {
|
||||
@@ -360,12 +344,11 @@ export function MessageBubble({
|
||||
return <TraceGroup message={message} />;
|
||||
}
|
||||
|
||||
if (message.role === "user" && message.sessionMessage?.direction === "incoming") {
|
||||
if (message.role === "user" && message.sessionMessage) {
|
||||
return (
|
||||
<IncomingSessionMessage
|
||||
message={message}
|
||||
showCopyAction={showCopyAction}
|
||||
sessionDirectory={sessionDirectory}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
/>
|
||||
);
|
||||
@@ -394,9 +377,6 @@ export function MessageBubble({
|
||||
cliApps={mentionCliApps}
|
||||
mcpPresets={mentionMcpPresets}
|
||||
sessionMentions={message.sessionMentions}
|
||||
sessionHandles={message.sessionHandles}
|
||||
attachedCliApps={message.cliApps}
|
||||
attachedMcpPresets={message.mcpPresets}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -405,9 +385,6 @@ export function MessageBubble({
|
||||
cliApps={mentionCliApps}
|
||||
mcpPresets={mentionMcpPresets}
|
||||
sessionMentions={message.sessionMentions}
|
||||
sessionHandles={message.sessionHandles}
|
||||
attachedCliApps={message.cliApps}
|
||||
attachedMcpPresets={message.mcpPresets}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
@@ -525,7 +502,6 @@ export function MessageBubble({
|
||||
streaming={!!message.isStreaming}
|
||||
preserveStreamingLayout
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
sessionHandles={sessionDirectory}
|
||||
>
|
||||
{message.content}
|
||||
</MarkdownText>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { InlineTokenHighlight } from "@/components/InlineTokenHighlight";
|
||||
import { sessionHandleColor } from "@/lib/session-handle";
|
||||
|
||||
export function SessionHandleLabel({
|
||||
id,
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<InlineTokenHighlight
|
||||
color={sessionHandleColor(id)}
|
||||
>
|
||||
{children}
|
||||
</InlineTokenHighlight>
|
||||
);
|
||||
}
|
||||
@@ -3,24 +3,14 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import {
|
||||
CapabilityMentionToken,
|
||||
SessionReferenceToken,
|
||||
splitCapabilityMentionSegments,
|
||||
splitSessionReferenceSegments,
|
||||
type CapabilityMentionSegment,
|
||||
type SessionReferenceSegment,
|
||||
} from "@/components/CliAppMentionText";
|
||||
import {
|
||||
INLINE_TOKEN_HIGHLIGHT_COLOR,
|
||||
InlineTokenHighlight,
|
||||
} from "@/components/InlineTokenHighlight";
|
||||
import type {
|
||||
CliAppInfo,
|
||||
McpPresetInfo,
|
||||
SessionHandle,
|
||||
SessionMention,
|
||||
UICliAppAttachment,
|
||||
UIMcpPresetAttachment,
|
||||
} from "@/lib/types";
|
||||
import type { CliAppInfo, McpPresetInfo, SessionMention } from "@/lib/types";
|
||||
|
||||
type SkillReferenceSegment =
|
||||
| { kind: "text"; text: string }
|
||||
@@ -28,7 +18,6 @@ type SkillReferenceSegment =
|
||||
|
||||
type UserMessageSegment =
|
||||
| CapabilityMentionSegment
|
||||
| SessionReferenceSegment
|
||||
| { kind: "skill"; text: string; name: string };
|
||||
|
||||
function splitSkillReferenceSegments(value: string): SkillReferenceSegment[] {
|
||||
@@ -60,75 +49,18 @@ function splitUserMessageSegments(
|
||||
cliApps: CliAppInfo[],
|
||||
mcpPresets: McpPresetInfo[],
|
||||
sessionMentions: SessionMention[],
|
||||
sessionHandles: SessionHandle[],
|
||||
attachedCliApps: UICliAppAttachment[],
|
||||
attachedMcpPresets: UIMcpPresetAttachment[],
|
||||
): UserMessageSegment[] {
|
||||
const segments: UserMessageSegment[] = [];
|
||||
const structuredAtNamespaces = new Map<string, "handle" | "cli" | "mcp">();
|
||||
sessionHandles.forEach((handle) => {
|
||||
structuredAtNamespaces.set(handle.name.toLowerCase(), "handle");
|
||||
});
|
||||
attachedCliApps.forEach((app) => {
|
||||
const name = app.name.toLowerCase();
|
||||
if (!structuredAtNamespaces.has(name)) structuredAtNamespaces.set(name, "cli");
|
||||
});
|
||||
attachedMcpPresets.forEach((preset) => {
|
||||
const name = preset.name.toLowerCase();
|
||||
if (!structuredAtNamespaces.has(name)) structuredAtNamespaces.set(name, "mcp");
|
||||
});
|
||||
const structuredAtNames = new Set(structuredAtNamespaces.keys());
|
||||
const replayCliApps = cliApps.filter((app) => {
|
||||
const owner = structuredAtNamespaces.get(app.name.toLowerCase());
|
||||
return owner === undefined || owner === "cli";
|
||||
});
|
||||
const replayMcpPresets = mcpPresets.filter((preset) => {
|
||||
const owner = structuredAtNamespaces.get(preset.name.toLowerCase());
|
||||
return owner === undefined || owner === "mcp";
|
||||
});
|
||||
const replaySessionHandles = sessionHandles.filter((handle) => (
|
||||
structuredAtNamespaces.get(handle.name.toLowerCase()) === "handle"
|
||||
));
|
||||
const hashSegments = splitSessionReferenceSegments(value, sessionMentions);
|
||||
const hashSessionKeys = new Set(hashSegments.flatMap((segment) => (
|
||||
segment.kind === "session" ? [segment.mention.session_key] : []
|
||||
)));
|
||||
const legacySessionMentions = sessionMentions.filter((mention) => (
|
||||
!hashSessionKeys.has(mention.session_key)
|
||||
&& !structuredAtNames.has(mention.name.toLowerCase())
|
||||
));
|
||||
|
||||
const appendCapabilitiesAndSkills = (text: string) => {
|
||||
for (const capability of splitCapabilityMentionSegments(
|
||||
text,
|
||||
replayCliApps,
|
||||
replayMcpPresets,
|
||||
replaySessionHandles,
|
||||
)) {
|
||||
if (capability.kind === "text") {
|
||||
segments.push(...splitSkillReferenceSegments(capability.text));
|
||||
} else {
|
||||
segments.push(capability);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const hashSegment of hashSegments) {
|
||||
if (hashSegment.kind === "session") {
|
||||
segments.push(hashSegment);
|
||||
continue;
|
||||
}
|
||||
for (const legacySegment of splitSessionReferenceSegments(
|
||||
hashSegment.text,
|
||||
legacySessionMentions,
|
||||
undefined,
|
||||
true,
|
||||
)) {
|
||||
if (legacySegment.kind === "session") {
|
||||
segments.push(legacySegment);
|
||||
} else {
|
||||
appendCapabilitiesAndSkills(legacySegment.text);
|
||||
}
|
||||
for (const segment of splitCapabilityMentionSegments(
|
||||
value,
|
||||
cliApps,
|
||||
mcpPresets,
|
||||
sessionMentions,
|
||||
)) {
|
||||
if (segment.kind === "text") {
|
||||
segments.push(...splitSkillReferenceSegments(segment.text));
|
||||
} else {
|
||||
segments.push(segment);
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
@@ -139,28 +71,14 @@ export function UserMessageText({
|
||||
cliApps,
|
||||
mcpPresets,
|
||||
sessionMentions = [],
|
||||
sessionHandles = [],
|
||||
attachedCliApps = [],
|
||||
attachedMcpPresets = [],
|
||||
}: {
|
||||
text: string;
|
||||
cliApps: CliAppInfo[];
|
||||
mcpPresets: McpPresetInfo[];
|
||||
sessionMentions?: SessionMention[];
|
||||
sessionHandles?: SessionHandle[];
|
||||
attachedCliApps?: UICliAppAttachment[];
|
||||
attachedMcpPresets?: UIMcpPresetAttachment[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const segments = splitUserMessageSegments(
|
||||
text,
|
||||
cliApps,
|
||||
mcpPresets,
|
||||
sessionMentions,
|
||||
sessionHandles,
|
||||
attachedCliApps,
|
||||
attachedMcpPresets,
|
||||
);
|
||||
const segments = splitUserMessageSegments(text, cliApps, mcpPresets, sessionMentions);
|
||||
return (
|
||||
<>
|
||||
{segments.map((segment, index) => {
|
||||
@@ -177,14 +95,6 @@ export function UserMessageText({
|
||||
{segment.name}
|
||||
</InlineTokenHighlight>
|
||||
);
|
||||
if (segment.kind === "session") return (
|
||||
<SessionReferenceToken
|
||||
key={`session-${segment.mention.session_key}-${index}`}
|
||||
mention={segment.mention}
|
||||
label={segment.text}
|
||||
variant="message"
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<CapabilityMentionToken
|
||||
key={`${segment.kind}-${index}`}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import { type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SessionHandleHighlight } from "@/components/CliAppMentionText";
|
||||
import { SessionHandleLabel } from "@/components/SessionHandleLabel";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -85,12 +85,11 @@ export function ThreadHeader({
|
||||
) : null}
|
||||
{handle ? (
|
||||
<span
|
||||
data-testid="thread-handle-handle"
|
||||
className="flex shrink-0 items-center rounded-md px-1.5 py-1 text-[12px] font-medium"
|
||||
>
|
||||
<SessionHandleHighlight handle={handle}>
|
||||
<SessionHandleLabel id={handle.id}>
|
||||
@{handle.name}
|
||||
</SessionHandleHighlight>
|
||||
</SessionHandleLabel>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,7 @@ import { MessageBubble } from "@/components/MessageBubble";
|
||||
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
|
||||
import { AssistantSelectionAction } from "@/components/thread/AssistantSelectionAction";
|
||||
import { normalizeActivityTimeline, type TurnUnit } from "@/lib/activity-timeline";
|
||||
import type {
|
||||
CliAppInfo,
|
||||
McpPresetInfo,
|
||||
SessionHandle,
|
||||
SlashCommand,
|
||||
UIMessage,
|
||||
} from "@/lib/types";
|
||||
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
|
||||
|
||||
interface ThreadMessagesProps {
|
||||
messages: UIMessage[];
|
||||
@@ -24,7 +18,6 @@ interface ThreadMessagesProps {
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
slashCommands?: SlashCommand[];
|
||||
sessionDirectory?: SessionHandle[];
|
||||
forkBoundaryMessageCount?: number | null;
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
onForkFromMessage?: (beforeUserIndex: number) => void;
|
||||
@@ -69,7 +62,6 @@ export function ThreadMessages({
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
slashCommands = [],
|
||||
sessionDirectory = [],
|
||||
forkBoundaryMessageCount = null,
|
||||
onOpenFilePreview,
|
||||
onForkFromMessage,
|
||||
@@ -167,7 +159,6 @@ export function ThreadMessages({
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
slashCommands={slashCommands}
|
||||
sessionDirectory={sessionDirectory}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
onForkFromMessage={onForkFromMessage}
|
||||
/>
|
||||
@@ -249,7 +240,6 @@ interface ThreadDisplayUnitProps {
|
||||
cliApps: CliAppInfo[];
|
||||
mcpPresets: McpPresetInfo[];
|
||||
slashCommands: SlashCommand[];
|
||||
sessionDirectory: SessionHandle[];
|
||||
onOpenFilePreview?: (path: string) => void;
|
||||
onForkFromMessage?: (beforeUserIndex: number) => void;
|
||||
}
|
||||
@@ -268,7 +258,6 @@ const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
|
||||
cliApps,
|
||||
mcpPresets,
|
||||
slashCommands,
|
||||
sessionDirectory,
|
||||
onOpenFilePreview,
|
||||
onForkFromMessage,
|
||||
}: ThreadDisplayUnitProps) {
|
||||
@@ -307,7 +296,6 @@ const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
slashCommands={slashCommands}
|
||||
sessionDirectory={sessionDirectory}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
onForkFromHere={forkIndex !== undefined ? onForkFromHere : undefined}
|
||||
/>
|
||||
@@ -336,7 +324,6 @@ function threadDisplayUnitPropsEqual(
|
||||
&& previous.cliApps === next.cliApps
|
||||
&& previous.mcpPresets === next.mcpPresets
|
||||
&& previous.slashCommands === next.slashCommands
|
||||
&& previous.sessionDirectory === next.sessionDirectory
|
||||
&& previous.onOpenFilePreview === next.onOpenFilePreview
|
||||
&& previous.onForkFromMessage === next.onForkFromMessage
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { FilePreviewAvailabilityProvider } from "@/components/FilePreviewAvailabilityContext";
|
||||
import { FilePreviewPanel } from "@/components/FilePreviewPanel";
|
||||
import { SessionHandleHighlight } from "@/components/CliAppMentionText";
|
||||
import { SessionHandleLabel } from "@/components/SessionHandleLabel";
|
||||
import { PromptNavigator } from "@/components/thread/PromptNavigator";
|
||||
import { SessionInfoPopover } from "@/components/thread/SessionInfoPopover";
|
||||
import { ThreadComposer } from "@/components/thread/ThreadComposer";
|
||||
@@ -37,7 +37,6 @@ import type { CanonicalRunSnapshot, StreamError } from "@/lib/nanobot-client";
|
||||
import { inferProviderFromModelName, providerDisplayLabel } from "@/lib/provider-brand";
|
||||
import type {
|
||||
ChatSummary,
|
||||
SessionHandle,
|
||||
SettingsPayload,
|
||||
SlashCommand,
|
||||
SkillSummary,
|
||||
@@ -639,28 +638,10 @@ export function ThreadShell({
|
||||
const { t } = useTranslation();
|
||||
const chatId = session?.chatId ?? null;
|
||||
const historyKey = temporary ? null : session?.key ?? null;
|
||||
const referenceSessions = useMemo(
|
||||
() => sessions.filter((candidate) => (
|
||||
candidate.key !== historyKey
|
||||
&& (
|
||||
workspaceScope?.access_mode !== "restricted"
|
||||
|| candidate.workspaceScope?.project_path === workspaceScope.project_path
|
||||
)
|
||||
)),
|
||||
[historyKey, sessions, workspaceScope],
|
||||
const mentionSessions = useMemo(
|
||||
() => sessions.filter((candidate) => candidate.key !== historyKey),
|
||||
[historyKey, sessions],
|
||||
);
|
||||
const handleSessions = useMemo(() => {
|
||||
if (temporary) return [];
|
||||
return sessions;
|
||||
}, [sessions, temporary]);
|
||||
const sessionDirectory = useMemo<SessionHandle[]>(() => {
|
||||
const handles = new Map<string, SessionHandle>();
|
||||
if (session?.handle) handles.set(session.handle.id, session.handle);
|
||||
for (const candidate of handleSessions) {
|
||||
if (candidate.handle) handles.set(candidate.handle.id, candidate.handle);
|
||||
}
|
||||
return [...handles.values()];
|
||||
}, [handleSessions, session?.handle]);
|
||||
const {
|
||||
messages: historical,
|
||||
loading,
|
||||
@@ -1330,14 +1311,7 @@ export function ThreadShell({
|
||||
setPendingFirstTargetChatId(newId);
|
||||
return true;
|
||||
},
|
||||
[
|
||||
booting,
|
||||
client,
|
||||
localModelPreset,
|
||||
onCreateChat,
|
||||
withWorkspaceScope,
|
||||
workspaceScope,
|
||||
],
|
||||
[booting, client, localModelPreset, onCreateChat, withWorkspaceScope, workspaceScope],
|
||||
);
|
||||
|
||||
const handleThreadSend = useCallback(
|
||||
@@ -1490,8 +1464,7 @@ export function ThreadShell({
|
||||
slashCommands={availableSlashCommands}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
sessions={referenceSessions}
|
||||
handleSessions={handleSessions}
|
||||
sessions={mentionSessions}
|
||||
skills={skills}
|
||||
onStop={stop}
|
||||
onTranscribeAudio={transcribeAudio}
|
||||
@@ -1538,8 +1511,7 @@ export function ThreadShell({
|
||||
slashCommands={availableSlashCommands}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
sessions={referenceSessions}
|
||||
handleSessions={handleSessions}
|
||||
sessions={mentionSessions}
|
||||
skills={skills}
|
||||
surfaceRef={composerSurfaceRef}
|
||||
onTranscribeAudio={transcribeAudio}
|
||||
@@ -1609,18 +1581,15 @@ export function ThreadShell({
|
||||
<div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
{hideHeaderTitle && !temporary && session?.handle ? (
|
||||
<div
|
||||
data-testid="pane-handle-identity"
|
||||
data-active={headerActive ? "true" : "false"}
|
||||
aria-label={`Session @${session.handle.name}`}
|
||||
className="flex h-8 shrink-0 items-center px-3 text-[12px]"
|
||||
>
|
||||
<span
|
||||
data-pane-handle-handle
|
||||
className="shrink-0"
|
||||
>
|
||||
<SessionHandleHighlight handle={session.handle}>
|
||||
<SessionHandleLabel id={session.handle.id}>
|
||||
@{session.handle.name}
|
||||
</SessionHandleHighlight>
|
||||
</SessionHandleLabel>
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1643,7 +1612,6 @@ export function ThreadShell({
|
||||
showScrollToBottomButton={!!session}
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
sessionDirectory={sessionDirectory}
|
||||
slashCommands={availableSlashCommands}
|
||||
forkBoundaryMessageCount={forkBoundaryMessageCount}
|
||||
hasMoreBefore={hasMoreBefore}
|
||||
|
||||
@@ -26,13 +26,7 @@ import {
|
||||
promptTop,
|
||||
} from "@/components/thread/promptNavigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
CliAppInfo,
|
||||
McpPresetInfo,
|
||||
SessionHandle,
|
||||
SlashCommand,
|
||||
UIMessage,
|
||||
} from "@/lib/types";
|
||||
import type { CliAppInfo, McpPresetInfo, SlashCommand, UIMessage } from "@/lib/types";
|
||||
|
||||
export interface ThreadViewportHandle {
|
||||
jumpToUserPrompt: (promptId: string) => void;
|
||||
@@ -56,7 +50,6 @@ interface ThreadViewportProps {
|
||||
cliApps?: CliAppInfo[];
|
||||
mcpPresets?: McpPresetInfo[];
|
||||
slashCommands?: SlashCommand[];
|
||||
sessionDirectory?: SessionHandle[];
|
||||
forkBoundaryMessageCount?: number | null;
|
||||
hasMoreBefore?: boolean;
|
||||
loadingOlder?: boolean;
|
||||
@@ -76,7 +69,6 @@ const SOFT_KEYBOARD_MIN_INSET_PX = 80;
|
||||
const SESSION_HANDOFF_EXIT_DURATION_MS = 80;
|
||||
const SESSION_HANDOFF_ENTER_DURATION_MS = 140;
|
||||
const SESSION_HANDOFF_OPACITY = 0.82;
|
||||
const EMPTY_SESSION_DIRECTORY: SessionHandle[] = [];
|
||||
export const INITIAL_HISTORY_WINDOW = 160;
|
||||
export const HISTORY_WINDOW_INCREMENT = 120;
|
||||
|
||||
@@ -112,6 +104,11 @@ function isKeyboardEditableElement(element: Element | null): element is HTMLElem
|
||||
].includes(element.type);
|
||||
}
|
||||
|
||||
function isThreadDisclosureTarget(target: EventTarget | null): boolean {
|
||||
return target instanceof Element
|
||||
&& target.closest("[data-thread-disclosure]") !== null;
|
||||
}
|
||||
|
||||
function isKeyboardControl(element: Element | null): boolean {
|
||||
return element instanceof HTMLElement
|
||||
&& element.closest(
|
||||
@@ -119,11 +116,6 @@ function isKeyboardControl(element: Element | null): boolean {
|
||||
) !== null;
|
||||
}
|
||||
|
||||
function isThreadDisclosureTarget(target: EventTarget | null): boolean {
|
||||
return target instanceof Element
|
||||
&& target.closest("[data-thread-disclosure]") !== null;
|
||||
}
|
||||
|
||||
type ThreadScrollDirection = "backward" | "forward";
|
||||
|
||||
const KEYBOARD_SCROLL_DIRECTIONS: Readonly<
|
||||
@@ -193,7 +185,6 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
cliApps = [],
|
||||
mcpPresets = [],
|
||||
slashCommands = [],
|
||||
sessionDirectory = EMPTY_SESSION_DIRECTORY,
|
||||
forkBoundaryMessageCount = null,
|
||||
hasMoreBefore = false,
|
||||
loadingOlder = false,
|
||||
@@ -771,7 +762,6 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
cliApps={cliApps}
|
||||
mcpPresets={mcpPresets}
|
||||
slashCommands={slashCommands}
|
||||
sessionDirectory={sessionDirectory}
|
||||
forkBoundaryMessageCount={visibleForkBoundaryMessageCount}
|
||||
onOpenFilePreview={onOpenFilePreview}
|
||||
onForkFromMessage={onForkFromMessage}
|
||||
|
||||
@@ -15,8 +15,6 @@ export interface ToolField {
|
||||
| "key"
|
||||
| "label"
|
||||
| "name"
|
||||
| "to"
|
||||
| "expect_reply"
|
||||
| "channel"
|
||||
| "chat_id"
|
||||
| "session_id"
|
||||
@@ -121,7 +119,7 @@ export function describeGenericToolRun(items: GenericToolRunItem[]): GenericTool
|
||||
status,
|
||||
label: activityLabel(family, status, collected, name, items),
|
||||
detail: activityDetail(items, family, name),
|
||||
aside: activityAside(items, family, name),
|
||||
aside: activityAside(items, family),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,7 +168,6 @@ function safeFields(args: unknown): ToolField[] {
|
||||
"key",
|
||||
"label",
|
||||
"name",
|
||||
"to",
|
||||
"channel",
|
||||
"chat_id",
|
||||
"session_id",
|
||||
@@ -181,17 +178,6 @@ function safeFields(args: unknown): ToolField[] {
|
||||
fields.push({ key, value: value.trim() });
|
||||
}
|
||||
}
|
||||
const expectReply = record.expect_reply;
|
||||
if (typeof expectReply === "boolean") {
|
||||
fields.push({ key: "expect_reply", value: String(expectReply) });
|
||||
} else if (typeof expectReply === "string") {
|
||||
const normalized = expectReply.toLowerCase();
|
||||
if (["true", "1", "yes"].includes(normalized)) {
|
||||
fields.push({ key: "expect_reply", value: "true" });
|
||||
} else if (["false", "0", "no"].includes(normalized)) {
|
||||
fields.push({ key: "expect_reply", value: "false" });
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
@@ -240,18 +226,6 @@ function activityLabel(
|
||||
return statusCopy(status, "Generating image", "Generated image", "Could not generate image");
|
||||
case "spawn":
|
||||
return statusCopy(status, "Delegating task", "Delegated task", "Could not delegate task");
|
||||
case "send_session_message":
|
||||
if (items.length > 1) {
|
||||
return statusCopy(
|
||||
status,
|
||||
"Sending messages",
|
||||
"Sent messages",
|
||||
"Could not send messages",
|
||||
);
|
||||
}
|
||||
return fieldValue(items[0]?.trace, "expect_reply") === "true"
|
||||
? statusCopy(status, "Asking", "Asked", "Could not reach")
|
||||
: statusCopy(status, "Sending to", "Sent to", "Could not reach");
|
||||
case "message":
|
||||
return statusCopy(status, "Sending message", "Sent message", "Could not send message");
|
||||
case "my":
|
||||
@@ -307,8 +281,6 @@ function activityDetail(items: GenericToolRunItem[], family: ToolFamily, name: s
|
||||
switch (name) {
|
||||
case "spawn":
|
||||
return safeText(fieldValue(trace, "label"));
|
||||
case "send_session_message":
|
||||
return safeText(fieldValue(trace, "to"));
|
||||
case "message":
|
||||
return safeText(fieldValue(trace, "channel"));
|
||||
case "my":
|
||||
@@ -329,15 +301,10 @@ function activityDetail(items: GenericToolRunItem[], family: ToolFamily, name: s
|
||||
}
|
||||
}
|
||||
|
||||
function activityAside(
|
||||
items: GenericToolRunItem[],
|
||||
family: ToolFamily,
|
||||
name: string,
|
||||
): string {
|
||||
function activityAside(items: GenericToolRunItem[], family: ToolFamily): string {
|
||||
const pathCount = uniqueValues(items, ["path", "file_path"]).length;
|
||||
if (pathCount > 1) return `${pathCount} files`;
|
||||
if (items.length <= 1) return "";
|
||||
if (name === "send_session_message") return `${items.length} messages`;
|
||||
if (family === "content-search" || family === "file-search" || family === "memory") {
|
||||
return `${items.length} searches`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user