From 925674354949750f121b2f2bcce25bb5deb48fcd Mon Sep 17 00:00:00 2001 From: LinYushen Date: Thu, 14 May 2026 11:52:13 +0800 Subject: [PATCH] fix(mention): prefetch squads so @mention list shows all squads Closes MUL-2176 --- .../agents/use-workspace-presence-prefetch.ts | 21 ++++++---- .../extensions/mention-suggestion.test.tsx | 41 +++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/core/agents/use-workspace-presence-prefetch.ts b/packages/core/agents/use-workspace-presence-prefetch.ts index 81b2d604f0..5d71a9a5b8 100644 --- a/packages/core/agents/use-workspace-presence-prefetch.ts +++ b/packages/core/agents/use-workspace-presence-prefetch.ts @@ -1,20 +1,22 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { agentListOptions } from "../workspace/queries"; +import { agentListOptions, squadListOptions } from "../workspace/queries"; import { runtimeListOptions } from "../runtimes/queries"; import { agentTaskSnapshotOptions } from "./queries"; -// Subscribe to the three queries that power agent presence so they're warm -// by the time any hover card / inline indicator first renders. Without this -// warm-up, surfaces that don't otherwise touch the snapshot (inbox, issues, -// chat) flash a skeleton on first hover while the fetch is in flight. +// Subscribe to the queries that power agent presence and the @mention +// suggestion list so they're warm by the time any hover card / inline +// indicator / mention popup first renders. Without this warm-up, surfaces +// that don't otherwise touch the snapshot (inbox, issues, chat) flash a +// skeleton on first hover while the fetch is in flight, and the @mention +// list may show incomplete results (e.g. missing squads). // -// useRealtimeSync (WS task / agent / daemon invalidations) and the 30s -// presence tick keep these caches fresh after the initial fetch — this hook -// only collapses the cold-start window. +// useRealtimeSync (WS task / agent / daemon / squad invalidations) and the +// 30s presence tick keep these caches fresh after the initial fetch — this +// hook only collapses the cold-start window. // -// All three are workspace-scoped; the queryKeys include wsId so workspace +// All queries are workspace-scoped; the queryKeys include wsId so workspace // switch automatically refetches the new workspace's data with no extra // wiring here. The workspace-scoped layouts on both apps gate rendering on // "workspace resolved", so callers can safely pass useWorkspaceId() — by the @@ -23,4 +25,5 @@ export function useWorkspacePresencePrefetch(wsId: string | undefined): void { useQuery({ ...agentListOptions(wsId ?? ""), enabled: !!wsId }); useQuery({ ...runtimeListOptions(wsId ?? ""), enabled: !!wsId }); useQuery({ ...agentTaskSnapshotOptions(wsId ?? ""), enabled: !!wsId }); + useQuery({ ...squadListOptions(wsId ?? ""), enabled: !!wsId }); } diff --git a/packages/views/editor/extensions/mention-suggestion.test.tsx b/packages/views/editor/extensions/mention-suggestion.test.tsx index e696c9a93d..dcd800bfec 100644 --- a/packages/views/editor/extensions/mention-suggestion.test.tsx +++ b/packages/views/editor/extensions/mention-suggestion.test.tsx @@ -61,11 +61,17 @@ function fakeQc(data: { visibility?: "workspace" | "private"; owner_id?: string | null; }>; + squads?: Array<{ + id: string; + name: string; + archived_at: string | null; + }>; issues?: Array<{ id: string; identifier: string; title: string; status: string }>; }): QueryClient { const map = new Map(); map.set(JSON.stringify(workspaceKeys.members("ws-1")), data.members ?? []); map.set(JSON.stringify(workspaceKeys.agents("ws-1")), data.agents ?? []); + map.set(JSON.stringify(workspaceKeys.squads("ws-1")), data.squads ?? []); const byStatus: ListIssuesCache["byStatus"] = {}; for (const status of PAGINATED_STATUSES) { const bucket = (data.issues ?? []).filter((i) => i.status === status); @@ -244,4 +250,39 @@ describe("createMentionSuggestion", () => { const items = result as MentionItem[]; expect(items.some((i) => i.type === "issue" && i.id === "i1")).toBe(true); }); + + it("includes all non-archived squads in the mention list", () => { + const qc = fakeQc({ + members: [{ user_id: "u1", name: "Alice", role: "member" }], + squads: [ + { id: "s1", name: "Jiayuan's Coding Team", archived_at: null }, + { id: "s2", name: "独立团", archived_at: null }, + { id: "s3", name: "Archived Squad", archived_at: "2026-01-01T00:00:00Z" }, + ], + }); + searchIssuesMock.mockReturnValue(new Promise(() => {})); + + const config = createMentionSuggestion(qc); + const result = config.items!({ query: "", editor: {} as never }); + + const items = result as MentionItem[]; + expect(items.filter((i) => i.type === "squad")).toHaveLength(2); + expect(items.some((i) => i.type === "squad" && i.label === "Jiayuan's Coding Team")).toBe(true); + expect(items.some((i) => i.type === "squad" && i.label === "独立团")).toBe(true); + expect(items.some((i) => i.type === "squad" && i.label === "Archived Squad")).toBe(false); + }); + + it("returns no squads when the squads cache is empty (not yet fetched)", () => { + const qc = fakeQc({ + members: [{ user_id: "u1", name: "Alice", role: "member" }], + // squads not provided — simulates cache miss + }); + searchIssuesMock.mockReturnValue(new Promise(() => {})); + + const config = createMentionSuggestion(qc); + const result = config.items!({ query: "", editor: {} as never }); + + const items = result as MentionItem[]; + expect(items.filter((i) => i.type === "squad")).toHaveLength(0); + }); });