Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/view/useAgentGUIWorkspaceReferencePicker.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

227 lines
9.4 KiB
TypeScript

import { useCallback, useMemo, useRef, useState } from "react";
import type {
ReferenceLocateTarget,
ReferenceNode,
WorkspaceFileReference
} from "@tutti-os/workspace-file-reference/contracts";
import type { ReferenceGroupedSelection } from "@tutti-os/workspace-file-reference/ui";
import { createRichTextMentionHref } from "@tutti-os/ui-rich-text/core";
import type { WorkspaceReferencePickResult } from "../AgentComposer";
import type {
AgentContextMentionItem,
AgentMentionWorkspaceReferenceItem
} from "../agentRichText/agentFileMentionExtension";
import type { AgentGUINodeViewProps } from "./AgentGUINodeView.types";
interface Input {
onWorkspaceFileReferencesAdded: AgentGUINodeViewProps["onWorkspaceFileReferencesAdded"];
projectDirectorySourceAggregator: AgentGUINodeViewProps["projectDirectorySourceAggregator"];
referenceSourceAggregator: AgentGUINodeViewProps["referenceSourceAggregator"];
resolveMentionReferenceTarget: AgentGUINodeViewProps["resolveMentionReferenceTarget"];
resolveWorkspaceReferenceInitialTarget: AgentGUINodeViewProps["resolveWorkspaceReferenceInitialTarget"];
viewModel: AgentGUINodeViewProps["viewModel"];
workspaceFileReferenceAdapter: AgentGUINodeViewProps["workspaceFileReferenceAdapter"];
workspaceFileReferenceCopy: AgentGUINodeViewProps["workspaceFileReferenceCopy"];
}
export function useAgentGUIWorkspaceReferencePicker(input: Input) {
const {
onWorkspaceFileReferencesAdded,
projectDirectorySourceAggregator,
referenceSourceAggregator,
resolveMentionReferenceTarget,
resolveWorkspaceReferenceInitialTarget,
viewModel,
workspaceFileReferenceAdapter,
workspaceFileReferenceCopy
} = input;
const [workspaceReferencePickerOpen, setWorkspaceReferencePickerOpen] =
useState(false);
const [workspaceReferencePickerPurpose, setWorkspaceReferencePickerPurpose] =
useState<"directory" | "reference">("reference");
// 打开引用 picker 时的定位目标(点任务/应用行的产物图标时设置;「+」按钮则为 null)。
const [workspaceReferencePickerTarget, setWorkspaceReferencePickerTarget] =
useState<ReferenceLocateTarget | null>(null);
const workspaceReferencePickerResolverRef = useRef<
((result: WorkspaceReferencePickResult) => void) | null
>(null);
const projectDirectoryPickerResolverRef = useRef<
((result: { path: string } | null) => void) | null
>(null);
const emptyReferencePickResult: WorkspaceReferencePickResult = useMemo(
() => ({ files: [], mentionItems: [] }),
[]
);
const hostLocalFileSourceId = "host-local-file";
const isWorkspaceReferencePickerNodeSelectable = useCallback(
(node: ReferenceNode) => {
if (workspaceReferencePickerPurpose === "directory") {
return node.kind === "folder";
}
return true;
},
[workspaceReferencePickerPurpose]
);
const requestWorkspaceReferences = useCallback(
async (
entity?: AgentContextMentionItem | null
): Promise<WorkspaceReferencePickResult> => {
if (!workspaceFileReferenceAdapter && !referenceSourceAggregator) {
return emptyReferencePickResult;
}
// 仅多源 picker(referenceSourceAggregator)支持定位;本地 picker 不支持。
const target =
entity && referenceSourceAggregator
? (resolveMentionReferenceTarget?.(entity) ?? null)
: referenceSourceAggregator
? (resolveWorkspaceReferenceInitialTarget?.({
activeConversation: viewModel.rail.activeConversation,
composerSelectedProjectPath:
viewModel.composer.composerSettings.selectedProjectPath ??
null,
userProjects: viewModel.rail.userProjects
}) ?? null)
: null;
setWorkspaceReferencePickerTarget(target);
setWorkspaceReferencePickerPurpose("reference");
setWorkspaceReferencePickerOpen(true);
return await new Promise<WorkspaceReferencePickResult>((resolve) => {
workspaceReferencePickerResolverRef.current?.(emptyReferencePickResult);
projectDirectoryPickerResolverRef.current?.(null);
projectDirectoryPickerResolverRef.current = null;
workspaceReferencePickerResolverRef.current = resolve;
});
},
[
emptyReferencePickResult,
referenceSourceAggregator,
resolveMentionReferenceTarget,
resolveWorkspaceReferenceInitialTarget,
viewModel.rail.activeConversation,
viewModel.composer.composerSettings.selectedProjectPath,
viewModel.rail.userProjects,
workspaceFileReferenceAdapter,
workspaceFileReferenceCopy
]
);
const requestProjectDirectory = useCallback(async () => {
if (!projectDirectorySourceAggregator) {
return null;
}
setWorkspaceReferencePickerTarget(null);
setWorkspaceReferencePickerPurpose("directory");
setWorkspaceReferencePickerOpen(true);
return await new Promise<{ path: string } | null>((resolve) => {
workspaceReferencePickerResolverRef.current?.(emptyReferencePickResult);
workspaceReferencePickerResolverRef.current = null;
projectDirectoryPickerResolverRef.current?.(null);
projectDirectoryPickerResolverRef.current = resolve;
});
}, [emptyReferencePickResult, projectDirectorySourceAggregator]);
const closeWorkspaceReferencePicker = useCallback(() => {
workspaceReferencePickerResolverRef.current?.(emptyReferencePickResult);
workspaceReferencePickerResolverRef.current = null;
projectDirectoryPickerResolverRef.current?.(null);
projectDirectoryPickerResolverRef.current = null;
setWorkspaceReferencePickerOpen(false);
setWorkspaceReferencePickerTarget(null);
setWorkspaceReferencePickerPurpose("reference");
}, [emptyReferencePickResult]);
const settleReferencePicker = useCallback(
(
result: WorkspaceReferencePickResult,
addedFiles: WorkspaceFileReference[]
) => {
workspaceReferencePickerResolverRef.current?.(result);
workspaceReferencePickerResolverRef.current = null;
setWorkspaceReferencePickerOpen(false);
setWorkspaceReferencePickerTarget(null);
setWorkspaceReferencePickerPurpose("reference");
if (addedFiles.length > 0) {
void onWorkspaceFileReferencesAdded?.(addedFiles);
}
},
[onWorkspaceFileReferencesAdded]
);
const confirmWorkspaceReferencePicker = useCallback(
(refs: WorkspaceFileReference[]) => {
if (workspaceReferencePickerPurpose === "directory") {
const directory = refs.find((ref) => ref.kind === "folder") ?? null;
projectDirectoryPickerResolverRef.current?.(
directory ? { path: directory.path } : null
);
projectDirectoryPickerResolverRef.current = null;
setWorkspaceReferencePickerOpen(false);
setWorkspaceReferencePickerTarget(null);
setWorkspaceReferencePickerPurpose("reference");
return;
}
settleReferencePicker({ files: refs, mentionItems: [] }, refs);
},
[settleReferencePicker, workspaceReferencePickerPurpose]
);
// 「文件夹=一个 reference 节点」确认:navigable 源文件夹折叠成 workspace-reference
// mention item(只携带可解析句柄 source+id+groupId,不展开文件);松散文件仍按 file
// mention 插入。agent 收到 `mention://workspace-reference/...` 后经 skill+CLI 按需解析。
const confirmWorkspaceReferenceBundles = useCallback(
(result: ReferenceGroupedSelection) => {
const workspaceRefs = result.files.filter(
(ref) => ref.sourceId !== hostLocalFileSourceId
);
const mentionItems: AgentMentionWorkspaceReferenceItem[] = result.bundles
.filter((bundle) => bundle.handle != null)
.map((bundle) => {
const handle = bundle.handle!;
const bundleIconUrl = bundle.iconUrl ?? undefined;
return {
kind: "workspace-reference",
href: createRichTextMentionHref({
providerId: "workspace-reference",
entityId: handle.id,
label: bundle.displayName,
scope: {
workspaceId: viewModel.shell.workspaceId,
source: handle.source,
...(handle.groupId?.trim()
? { groupId: handle.groupId.trim() }
: {}),
...(bundle.fileCount > 0
? { count: String(bundle.fileCount) }
: {})
}
}),
workspaceId: viewModel.shell.workspaceId,
targetId: handle.id,
source: handle.source,
...(handle.groupId ? { groupId: handle.groupId } : {}),
name: bundle.displayName,
iconUrl: bundleIconUrl,
fileCount: bundle.fileCount
};
});
// bundle 不再展开文件,仅松散文件计入「最近引用」跟踪。
settleReferencePicker(
{ files: result.files, mentionItems },
workspaceRefs
);
},
[hostLocalFileSourceId, settleReferencePicker]
);
return {
closeWorkspaceReferencePicker,
confirmWorkspaceReferenceBundles,
confirmWorkspaceReferencePicker,
isWorkspaceReferencePickerNodeSelectable,
requestProjectDirectory,
requestWorkspaceReferences,
workspaceReferencePickerAggregator:
workspaceReferencePickerPurpose === "directory"
? (projectDirectorySourceAggregator ?? null)
: (referenceSourceAggregator ?? null),
workspaceReferencePickerOpen,
workspaceReferencePickerPurpose,
workspaceReferencePickerTarget
};
}