mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-27 06:56:17 +00:00
* feat(frontend): add side conversations for quoted follow-ups * style(frontend): apply prettier formatting to sidecar-chat files * fix(frontend): surface sidecar cascade cleanup failures via console.warn Previously deleteSidecarThreadsForParent silently swallowed both lookup errors and per-thread deletion failures, so parent thread deletions could succeed while orphaning sidecar threads with no signal to the caller. Log a warning that includes the parent id and the failed thread ids/reasons so the leak is discoverable in telemetry, matching the existing console.warn/error pattern in this file. * fix(frontend): address all sidecar review feedback Resolve every reviewer comment on PR #3934: - input-box/hooks/sidecar-panel: clear quoted references only via an `onSent` callback that fires after the in-flight guard, so a dropped send no longer silently discards quotes (willem-bd #3550). - message-list: flip the selection toolbar below the selection when it would clip above the viewport (willem-bd #3551). - reference-metadata/thread/input-box: keep referenced ids, roles, and count arrays 1:1 parallel instead of deduping ids (willem-bd #3552). - message-list: widen selection containment to the shared assistant-turn container and hint when a selection crosses messages (willem-bd #3553). - sidecar/api: coalesce concurrent sidecar creates for one parent behind a single in-flight promise to prevent duplicates (willem-bd #3554). - sidecar-trigger/context: force-restore on trigger click so a sidecar deleted elsewhere self-heals instead of opening a dead thread (willem-bd #3555). - threads/hooks: surface sidecar cascade cleanup failures via console.warn for both lookup and per-thread deletes (Copilot). Add unit + e2e coverage for parallel metadata, atomic create, and trigger self-healing.
101 lines
2.7 KiB
TypeScript
101 lines
2.7 KiB
TypeScript
import { getAPIClient } from "@/core/api";
|
|
import { fetch as fetchWithAuth } from "@/core/api/fetcher";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
import type { AgentThread } from "@/core/threads";
|
|
|
|
import type { SidecarContext } from "./context";
|
|
import {
|
|
SIDECAR_METADATA_KEY,
|
|
buildSidecarThreadMetadata,
|
|
isSidecarThread,
|
|
} from "./thread";
|
|
|
|
type SidecarThreadSearchClient = {
|
|
threads: {
|
|
search: (query: Record<string, unknown>) => Promise<AgentThread[]>;
|
|
};
|
|
};
|
|
|
|
// The find-then-create flow is two independent round-trips with no backend
|
|
// upsert, so a double-click on "Ask in side chat" or two callers racing on the
|
|
// same parent thread can each create a duplicate sidecar thread. Coalesce
|
|
// concurrent creates for the same parent behind a single in-flight promise so
|
|
// only one thread is created; the entry is cleared once it settles.
|
|
const inFlightCreates = new Map<string, Promise<AgentThread>>();
|
|
|
|
export async function createSidecarThread({
|
|
parentThreadId,
|
|
context,
|
|
}: {
|
|
parentThreadId: string;
|
|
context: SidecarContext | SidecarContext[];
|
|
}): Promise<AgentThread> {
|
|
const inFlight = inFlightCreates.get(parentThreadId);
|
|
if (inFlight) {
|
|
return inFlight;
|
|
}
|
|
|
|
const request = createSidecarThreadRequest({ parentThreadId, context });
|
|
inFlightCreates.set(parentThreadId, request);
|
|
try {
|
|
return await request;
|
|
} finally {
|
|
if (inFlightCreates.get(parentThreadId) === request) {
|
|
inFlightCreates.delete(parentThreadId);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createSidecarThreadRequest({
|
|
parentThreadId,
|
|
context,
|
|
}: {
|
|
parentThreadId: string;
|
|
context: SidecarContext | SidecarContext[];
|
|
}): Promise<AgentThread> {
|
|
const response = await fetchWithAuth(`${getBackendBaseURL()}/api/threads`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
metadata: buildSidecarThreadMetadata(parentThreadId, context),
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to create side conversation.");
|
|
}
|
|
|
|
return (await response.json()) as AgentThread;
|
|
}
|
|
|
|
export async function findLatestSidecarThread({
|
|
parentThreadId,
|
|
isMock,
|
|
apiClient = getAPIClient(isMock) as SidecarThreadSearchClient,
|
|
}: {
|
|
parentThreadId: string;
|
|
isMock?: boolean;
|
|
apiClient?: SidecarThreadSearchClient;
|
|
}): Promise<AgentThread | null> {
|
|
const response = await apiClient.threads.search({
|
|
metadata: {
|
|
[SIDECAR_METADATA_KEY]: true,
|
|
parent_thread_id: parentThreadId,
|
|
},
|
|
limit: 1,
|
|
offset: 0,
|
|
sortBy: "updated_at",
|
|
sortOrder: "desc",
|
|
});
|
|
|
|
return (
|
|
response.find(
|
|
(thread) =>
|
|
isSidecarThread(thread) &&
|
|
thread.metadata?.parent_thread_id === parentThreadId,
|
|
) ?? null
|
|
);
|
|
}
|