refactor: simplify cross-session messaging
This commit is contained in:
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