Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/AgentMentionSearchController.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

800 lines
25 KiB
TypeScript

import {
DEFAULT_AGENT_MENTION_FILTER,
DEFAULT_MENTION_GROUP_PAGE_SIZE,
mentionGroupPageSize
} from "./agentMentionSearchHelpers";
import type { AgentContextMentionItem } from "./agentRichText/agentFileMentionExtension";
import type { AgentContextMentionProvider } from "./agentContextMentionProvider";
import {
buildBrowseCategories,
FILE_PROVIDER_ID,
WORKSPACE_ISSUE_PROVIDER_ID,
type AgentMentionFilterId,
type AgentMentionGroupId,
type AgentMentionSearchListener
} from "./AgentMentionSearchContracts";
import {
scheduleAgentMentionIdleTask,
mergeAgentMentionBrowseIssueGroupPage,
MAX_BROWSE_CACHE_ENTRIES,
resetAgentMentionSearchBrowseCacheForTests
} from "./AgentMentionSearchCache";
import { diagnosticErrorKind } from "./AgentMentionSearchModel";
import {
queryAgentMentionProviderWithDiagnostics,
type AgentMentionProviderQueryDiagnostic
} from "./agentMentionSearchDiagnostics";
import {
queryAgentMentionProviderDirectoryItems,
queryAgentMentionProviderGroupPage
} from "./AgentMentionSearchIndex";
import { AgentMentionSearchControllerBase } from "./AgentMentionSearchControllerBase";
import { AgentMentionDirectoryRequestLifecycle } from "./AgentMentionDirectoryRequestLifecycle";
import type {
ReferenceProvenanceCatalog,
ReferenceProvenanceFilter
} from "@tutti-os/workspace-file-reference/contracts";
import { presentWorkspaceFileDirectoryMentionItems } from "./agentMentionWorkspaceFilesPresentation";
import { AgentMentionProvenanceFilterState } from "./AgentMentionProvenanceFilterState";
import { resolveAgentMentionQueryUpdate } from "./AgentMentionQueryUpdate";
export type {
AgentMentionBrowseCategory,
AgentMentionFilterId,
AgentMentionGroup,
AgentMentionGroupId,
AgentMentionSearchState
} from "./AgentMentionSearchContracts";
export { MAX_BROWSE_CACHE_ENTRIES, resetAgentMentionSearchBrowseCacheForTests };
export class AgentMentionSearchController extends AgentMentionSearchControllerBase {
private readonly issueLoadMoreRequests = new Map<
string,
{ abortController: AbortController; token: symbol }
>();
private readonly directoryRequestLifecycle =
new AgentMentionDirectoryRequestLifecycle();
private readonly provenanceFilters = new AgentMentionProvenanceFilterState();
setProvenanceCatalog(catalog: ReferenceProvenanceCatalog | null): void {
if (this.currentProvenanceCatalog === catalog) return;
this.currentProvenanceCatalog = catalog;
if (
this.currentFilter !== "session" ||
(this.state.status !== "ready" && this.state.status !== "loading")
) {
return;
}
this.setState({
...this.state,
groups: this.groupsFromRawGroups()
});
}
setProvenanceFilters(
filters: Record<AgentMentionFilterId, ReferenceProvenanceFilter | null>
): void {
const next = this.provenanceFilters.replace(filters, this.currentFilter);
if (!next.changed) return;
this.cancelPendingPreload();
this.currentProvenanceFilter = next.value;
this.resetWorkspaceFileBrowsePaths();
this.updateQuery({
workspaceId: this.activeWorkspaceId,
currentUserId: this.currentUserId,
query: this.currentQuery,
sectionKey: this.currentSectionKey,
sessionCwd: this.currentSessionCwd
});
}
subscribe(listener: AgentMentionSearchListener): () => void {
this.listeners.add(listener);
listener(this.state);
return () => {
this.listeners.delete(listener);
};
}
updateQuery(input: {
workspaceId: string;
currentUserId?: string | null;
query: string;
sectionKey?: string | null;
sessionCwd?: string | null;
}): void {
if (this.disposed) return;
const next = resolveAgentMentionQueryUpdate(input, {
agentGeneratedBrowsePath: this.agentGeneratedBrowsePath,
workspaceId: this.activeWorkspaceId,
currentUserId: this.currentUserId,
sectionKey: this.currentSectionKey,
sessionCwd: this.currentSessionCwd,
query: this.currentQuery,
filter: this.currentFilter,
directoryDepth: this.workspaceFileBrowsePaths.length
});
if (next.ignore) return;
this.activeWorkspaceId = next.workspaceId;
this.currentUserId = next.currentUserId;
this.currentSectionKey = next.sectionKey;
this.currentSessionCwd = next.sessionCwd;
this.currentQuery = next.query;
this.clearTimer();
this.abortActiveRequest();
this.directoryRequestLifecycle.cancel();
this.cancelIssueLoadMoreRequests();
const requestId = ++this.requestId;
this.resetAgentGeneratedBrowsePath();
this.resetWorkspaceFileBrowsePaths();
this.resetExpandedCounts();
this.resetSearchLimits();
this.resetRawGroups();
if (!this.activeWorkspaceId) {
this.setState({
status: "idle",
query: this.currentQuery,
mode: "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: [],
error: null
});
return;
}
if (!this.currentQuery) {
this.startBrowseModeFetch(this.currentFilter);
return;
}
this.setState({
status: "loading",
query: this.currentQuery,
mode: "results",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
const abortSignal = this.beginActiveRequest();
this.timer = this.scheduler.schedule(this.debounceMs, () => {
void this.runSearch({
workspaceId: this.activeWorkspaceId,
currentUserId: this.currentUserId,
query: this.currentQuery,
requestId,
filter: this.currentFilter,
provenanceFilter: this.currentProvenanceFilter,
sectionKey: this.currentSectionKey,
sessionCwd: this.currentSessionCwd,
abortSignal
});
});
}
setFilter(filter: AgentMentionFilterId): void {
if (this.disposed) {
return;
}
this.currentFilter = filter;
this.currentProvenanceFilter = this.provenanceFilters.value(filter);
this.clearTimer();
this.abortActiveRequest();
this.directoryRequestLifecycle.cancel();
this.cancelIssueLoadMoreRequests();
const requestId = ++this.requestId;
this.resetAgentGeneratedBrowsePath();
this.resetWorkspaceFileBrowsePaths();
this.resetExpandedCounts();
this.resetSearchLimits();
this.resetRawGroups();
if (!this.currentQuery) {
this.startBrowseModeFetch(filter);
return;
}
if (!this.activeWorkspaceId) {
this.setState({
status: "ready",
query: this.currentQuery,
mode: "results",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: [],
error: null
});
return;
}
this.setState({
status: "loading",
query: this.currentQuery,
mode: "results",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
const abortSignal = this.beginActiveRequest();
void this.runSearch({
workspaceId: this.activeWorkspaceId,
currentUserId: this.currentUserId,
query: this.currentQuery,
requestId,
filter,
provenanceFilter: this.currentProvenanceFilter,
sectionKey: this.currentSectionKey,
sessionCwd: this.currentSessionCwd,
abortSignal
});
}
preloadBrowse(input: {
workspaceId: string;
currentUserId?: string | null;
sectionKey?: string | null;
sessionCwd?: string | null;
filter?: AgentMentionFilterId;
}): void {
if (this.disposed) {
return;
}
const workspaceId = input.workspaceId.trim();
if (!workspaceId) {
return;
}
const filter = input.filter ?? DEFAULT_AGENT_MENTION_FILTER;
const currentUserId = input.currentUserId?.trim() ?? "";
const sectionKey = input.sectionKey?.trim() ?? "";
const sessionCwd = input.sessionCwd?.trim() ?? "";
const provenanceFilter = this.currentProvenanceFilter;
const cacheKey = this.browseCacheKey(
{ currentUserId, filter, sectionKey, sessionCwd, workspaceId },
provenanceFilter
);
if (this.readBrowseCache(cacheKey).isFresh) {
return;
}
if (this.pendingPreloadKey === cacheKey) {
return;
}
this.cancelPendingPreload();
this.pendingPreloadKey = cacheKey;
this.preloadCancel = scheduleAgentMentionIdleTask(() => {
this.preloadCancel = null;
this.pendingPreloadKey = null;
if (this.disposed) {
return;
}
this.runBrowsePreload({
cacheKey,
currentUserId,
filter,
provenanceFilter,
sectionKey,
sessionCwd,
workspaceId
});
});
}
private runBrowsePreload(input: {
cacheKey: string;
currentUserId: string;
filter: AgentMentionFilterId;
provenanceFilter: ReferenceProvenanceFilter | null;
sectionKey: string;
sessionCwd: string;
workspaceId: string;
}): void {
const {
cacheKey,
currentUserId,
filter,
provenanceFilter,
sectionKey,
sessionCwd,
workspaceId
} = input;
this.logLifecycle("browse.preload", {
filter,
providerIds: this.providerIdsForDiagnostics(),
sessionCwdPresent: Boolean(sessionCwd),
workspaceId
});
const cached = this.readBrowseCache(cacheKey);
this.logBrowseCacheState({
cacheKey,
cached,
filter,
reason: "preload",
workspaceId
});
if (cached.isFresh) {
return;
}
void this.loadBrowseFetchResult(
{
workspaceId,
currentUserId,
filter,
sectionKey,
sessionCwd
},
cacheKey,
"preload",
provenanceFilter
).catch((error) => {
this.logLifecycle("browse.fetch.error", {
errorKind: diagnosticErrorKind(error),
filter,
reason: "preload",
workspaceId
});
});
}
protected cancelPendingPreload(): void {
if (this.preloadCancel) {
this.preloadCancel();
this.preloadCancel = null;
}
this.pendingPreloadKey = null;
}
enterCategory(category: AgentMentionFilterId): void {
this.setFilter(category);
}
selectFileMentionNavigationItem(item: AgentContextMentionItem): boolean {
if (item.kind !== "file" || !item.mentionNavigation) {
return false;
}
if (item.mentionNavigation === "agent-generated-folder") {
this.agentGeneratedBrowsePath = item.path;
this.expandedCounts.agent_generated_files = mentionGroupPageSize(
this.currentFilter,
"agent_generated_files"
);
this.setState({
status: this.state.status === "loading" ? "loading" : "ready",
query: this.currentQuery,
mode: this.currentQuery ? "results" : "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
return true;
}
if (item.mentionNavigation === "agent-generated-folder-back") {
this.resetAgentGeneratedBrowsePath();
this.setState({
status: this.state.status === "loading" ? "loading" : "ready",
query: this.currentQuery,
mode: this.currentQuery ? "results" : "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
return true;
}
if (item.mentionNavigation === "workspace-folder") {
const path = item.path.trim();
if (!path || this.currentFilter !== "file" || this.currentQuery) {
return false;
}
this.workspaceFileBrowsePaths.push(path);
this.loadWorkspaceFileDirectory(path);
return true;
}
if (item.mentionNavigation === "workspace-folder-back") {
return this.exitWorkspaceFileBrowse();
}
return false;
}
exitFileMentionBrowse(): boolean {
if (this.workspaceFileBrowsePaths.length > 0) {
return this.exitWorkspaceFileBrowse();
}
if (!this.agentGeneratedBrowsePath) return false;
this.resetAgentGeneratedBrowsePath();
this.setState({
status: this.state.status === "loading" ? "loading" : "ready",
query: this.currentQuery,
mode: this.currentQuery ? "results" : "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
return true;
}
private exitWorkspaceFileBrowse(): boolean {
if (this.workspaceFileBrowsePaths.length === 0) {
return false;
}
this.workspaceFileBrowsePaths.pop();
const parentPath = this.workspaceFileBrowsePaths.at(-1);
if (parentPath) {
this.loadWorkspaceFileDirectory(parentPath);
return true;
}
this.directoryRequestLifecycle.cancel();
this.startBrowseModeFetch("file");
return true;
}
private loadWorkspaceFileDirectory(directoryPath: string): void {
this.clearTimer();
this.abortActiveRequest();
this.cancelIssueLoadMoreRequests();
this.requestId += 1;
const workspaceId = this.activeWorkspaceId;
const provider = this.contextMentionProviders.get(FILE_PROVIDER_ID);
const directoryRequest = this.directoryRequestLifecycle.start();
this.setState({
status: "loading",
query: "",
mode: "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
void queryAgentMentionProviderDirectoryItems({
abortSignal: directoryRequest.abortSignal,
provider,
directoryPath,
workspaceId,
currentUserId: this.currentUserId,
sectionKey: this.currentSectionKey,
sessionCwd: this.currentSessionCwd,
provenanceFilter: this.currentProvenanceFilter
})
.then((items) => {
if (
!this.directoryRequestLifecycle.canApply({
activeDirectoryPath: this.workspaceFileBrowsePaths.at(-1),
activeWorkspaceId: this.activeWorkspaceId,
directoryPath,
disposed: this.disposed,
filter: this.currentFilter,
query: this.currentQuery,
requestId: directoryRequest.requestId,
workspaceId
})
) {
return;
}
this.directoryRequestLifecycle.finish(directoryRequest.requestId);
this.rawGroups.opened_files = presentWorkspaceFileDirectoryMentionItems(
{
browsePath: directoryPath,
items
}
);
this.totalCounts.opened_files = this.rawGroups.opened_files.length;
this.setState({
status: "ready",
query: "",
mode: "browse",
filter: "file",
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
})
.catch((error) => {
if (
!this.directoryRequestLifecycle.canApply({
activeDirectoryPath: this.workspaceFileBrowsePaths.at(-1),
activeWorkspaceId: this.activeWorkspaceId,
directoryPath,
disposed: this.disposed,
filter: this.currentFilter,
query: this.currentQuery,
requestId: directoryRequest.requestId,
workspaceId
})
) {
return;
}
this.directoryRequestLifecycle.finish(directoryRequest.requestId);
this.setState({
status: "error",
query: "",
mode: "browse",
filter: "file",
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: error instanceof Error ? error.message : String(error)
});
});
}
expandGroup(groupId: AgentMentionGroupId): void {
if (groupId.startsWith("issue-topic:")) {
this.loadMoreIssueTopic(groupId as `issue-topic:${string}`);
return;
}
const pageSize = mentionGroupPageSize(this.currentFilter, groupId);
const current = this.expandedCounts[groupId] ?? pageSize;
this.expandedCounts[groupId] = current + pageSize;
if (!this.currentQuery) {
this.setState({
status: "ready",
query: "",
mode: "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
return;
}
const needsMoreFiles =
(groupId === "opened_files" ||
groupId === "files" ||
groupId === "agent_generated_files") &&
(groupId === "agent_generated_files"
? this.rawGroups.agent_generated_files.length >=
this.currentFileSearchLimit
: this.rawGroups.opened_files.length >= this.currentFileSearchLimit);
const needsMoreIssues =
groupId === "issues" &&
(this.totalCounts.issues ?? this.rawGroups.issues.length) >
this.rawGroups.issues.length;
if (needsMoreFiles) {
this.currentFileSearchLimit += DEFAULT_MENTION_GROUP_PAGE_SIZE;
}
if (needsMoreIssues) {
this.currentIssueSearchLimit += DEFAULT_MENTION_GROUP_PAGE_SIZE;
}
if (needsMoreFiles || needsMoreIssues) {
this.clearTimer();
this.abortActiveRequest();
const requestId = ++this.requestId;
this.setState({
status: "loading",
query: this.currentQuery,
mode: "results",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
const abortSignal = this.beginActiveRequest();
void this.runSearch({
workspaceId: this.activeWorkspaceId,
currentUserId: this.currentUserId,
query: this.currentQuery,
requestId,
filter: this.currentFilter,
provenanceFilter: this.currentProvenanceFilter,
sectionKey: this.currentSectionKey,
sessionCwd: this.currentSessionCwd,
abortSignal
});
return;
}
this.setState({
status: "ready",
query: this.currentQuery,
mode: "results",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
}
close(): void {
this.clearTimer();
this.abortActiveRequest();
this.directoryRequestLifecycle.cancel();
this.cancelIssueLoadMoreRequests();
this.requestId += 1;
this.currentFilter = DEFAULT_AGENT_MENTION_FILTER;
this.resetAgentGeneratedBrowsePath();
this.resetWorkspaceFileBrowsePaths();
this.resetExpandedCounts();
this.resetSearchLimits();
this.resetRawGroups();
this.currentQuery = "";
this.setState({
status: "idle",
query: "",
mode: "browse",
filter: DEFAULT_AGENT_MENTION_FILTER,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: [],
error: null
});
}
dispose(): void {
this.disposed = true;
this.clearTimer();
this.abortActiveRequest();
this.directoryRequestLifecycle.cancel();
this.cancelIssueLoadMoreRequests();
this.cancelPendingPreload();
this.listeners.clear();
this.requestId += 1;
}
private loadMoreIssueTopic(groupId: `issue-topic:${string}`): void {
const group = this.issueTopicGroups?.find(
(candidate) => candidate.id === groupId
);
const cursor = group?.nextPageToken;
const provider = this.contextMentionProviders.get(
WORKSPACE_ISSUE_PROVIDER_ID
);
if (!group || !cursor || !provider?.queryGroupPage) {
return;
}
const requestKey = JSON.stringify({
workspaceId: this.activeWorkspaceId,
query: this.currentQuery,
providerGroupId: group.providerGroupId,
cursor
});
if (this.issueLoadMoreRequests.has(requestKey)) {
return;
}
const requestId = this.requestId;
const workspaceId = this.activeWorkspaceId;
const currentUserId = this.currentUserId;
const sectionKey = this.currentSectionKey;
const sessionCwd = this.currentSessionCwd;
const provenanceFilter = this.currentProvenanceFilter;
const query = this.currentQuery;
const filter = this.currentFilter;
const abortController = new AbortController();
const requestToken = Symbol(requestKey);
group.loadMoreStatus = "loading";
group.loadMoreError = null;
this.emitIssueTopicGroupsState();
void (async () => {
try {
const diagnostics: AgentMentionProviderQueryDiagnostic[] = [];
const page = await queryAgentMentionProviderWithDiagnostics({
abortSignal: abortController.signal,
diagnosticNow: this.diagnosticNow,
diagnostics,
fallback: null,
providerId: WORKSPACE_ISSUE_PROVIDER_ID,
providerTimeoutMs: this.providerTimeoutMs,
throwOnTimeout: true,
query: (abortSignal) =>
queryAgentMentionProviderGroupPage({
provider,
providerGroupId: group.providerGroupId,
workspaceId,
currentUserId,
query,
cursor,
pageSize: DEFAULT_MENTION_GROUP_PAGE_SIZE,
sectionKey,
sessionCwd,
abortSignal,
provenanceFilter
}),
resultCount: (result) => result?.items.length ?? 0
});
if (
!page ||
!this.canApply(requestId, workspaceId, query, filter) ||
abortController.signal.aborted
) {
return;
}
const current = this.issueTopicGroups?.find(
(candidate) => candidate.id === groupId
);
if (!current || current.nextPageToken !== cursor) {
return;
}
const seen = new Set(
current.items
.filter((item) => item.kind === "workspace-issue")
.map((item) => item.targetId)
);
const appended = page.items.filter((item) => {
if (item.kind !== "workspace-issue") {
return true;
}
if (seen.has(item.targetId)) {
return false;
}
seen.add(item.targetId);
return true;
});
current.items = [...current.items, ...appended];
current.totalCount = Math.max(page.totalCount, current.items.length);
current.nextPageToken = page.nextPageToken;
current.loadMoreStatus = "idle";
current.loadMoreError = null;
if (!query) {
mergeAgentMentionBrowseIssueGroupPage({
cacheKey: this.browseCacheKey(
{
workspaceId,
currentUserId,
filter,
sectionKey,
sessionCwd
},
provenanceFilter
),
group: page,
cachedAt: this.diagnosticNow()
});
}
this.emitIssueTopicGroupsState();
} catch (error) {
if (
!this.canApply(requestId, workspaceId, query, filter) ||
abortController.signal.aborted
) {
return;
}
const current = this.issueTopicGroups?.find(
(candidate) => candidate.id === groupId
);
if (current?.nextPageToken === cursor) {
current.loadMoreStatus = "error";
current.loadMoreError =
error instanceof Error ? error.message : String(error);
this.emitIssueTopicGroupsState();
}
} finally {
const active = this.issueLoadMoreRequests.get(requestKey);
if (active?.token === requestToken) {
this.issueLoadMoreRequests.delete(requestKey);
}
}
})();
this.issueLoadMoreRequests.set(requestKey, {
abortController,
token: requestToken
});
}
private emitIssueTopicGroupsState(): void {
this.setState({
status: "ready",
query: this.currentQuery,
mode: this.currentQuery ? "results" : "browse",
filter: this.currentFilter,
categories: buildBrowseCategories(this.hiddenFilterIds),
groups: this.groupsFromRawGroups(),
error: null
});
}
private cancelIssueLoadMoreRequests(): void {
for (const request of this.issueLoadMoreRequests.values()) {
request.abortController.abort();
}
this.issueLoadMoreRequests.clear();
}
}
// A transient controller warms the shared cache and leaves no timer behind.
export function preloadAgentMentionBrowse(input: {
workspaceId: string;
currentUserId?: string | null;
sectionKey?: string | null;
sessionCwd?: string | null;
contextMentionProviders?: readonly AgentContextMentionProvider[];
filter?: AgentMentionFilterId;
}): void {
const controller = new AgentMentionSearchController({
contextMentionProviders: input.contextMentionProviders
});
controller.preloadBrowse({
workspaceId: input.workspaceId,
currentUserId: input.currentUserId,
sectionKey: input.sectionKey,
sessionCwd: input.sessionCwd,
filter: input.filter
});
}