refactor: simplify cross-session messaging

This commit is contained in:
chengyongru
2026-08-19 01:15:56 +08:00
committed by chengyongru
parent 0e184965e8
commit 251a1ccd40
78 changed files with 1578 additions and 7569 deletions
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -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>
+1 -14
View File
@@ -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
);
+9 -41
View File
@@ -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}
+6 -16
View File
@@ -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`;
}