@tutti-os/workspace-file-reference 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import {
4
4
  nodeRefKey,
5
5
  sortReferenceNodes,
6
6
  uniqueWorkspaceFileReferences
7
- } from "./chunk-XA5U53VP.js";
7
+ } from "./chunk-LAYTHZN7.js";
8
8
 
9
9
  // src/react/internal/reference/WorkspaceFileReferencePickerState.ts
10
10
  var workspaceFileReferenceDefaultExpandedDepth = 4;
@@ -1610,4 +1610,4 @@ export {
1610
1610
  ROOT_CHILDREN_KEY,
1611
1611
  createReferenceSourcePickerController
1612
1612
  };
1613
- //# sourceMappingURL=chunk-C3G4ADEJ.js.map
1613
+ //# sourceMappingURL=chunk-CT42WDOK.js.map
@@ -39,7 +39,8 @@ function selectedReferenceToWorkspaceFileReference(selected) {
39
39
  return {
40
40
  path: selected.path,
41
41
  kind: selected.kind,
42
- ...selected.displayName ? { displayName: selected.displayName } : {}
42
+ ...selected.displayName ? { displayName: selected.displayName } : {},
43
+ ...selected.sourceId ? { sourceId: selected.sourceId } : {}
43
44
  };
44
45
  }
45
46
 
@@ -109,7 +110,11 @@ function createReferenceSourceAggregator(registry) {
109
110
  return await source.readPreview?.(scope, node) ?? null;
110
111
  },
111
112
  resolveSelection(node) {
112
- return requireSource(node.ref.sourceId).resolveSelection(node);
113
+ const selected = requireSource(node.ref.sourceId).resolveSelection(node);
114
+ return {
115
+ ...selected,
116
+ sourceId: selected.sourceId ?? node.ref.sourceId
117
+ };
113
118
  },
114
119
  async locateTarget(scope, sourceId, params) {
115
120
  if (!sourceMap.has(sourceId)) {
@@ -402,4 +407,4 @@ export {
402
407
  matchesFilterCategories,
403
408
  uniqueWorkspaceFileReferences
404
409
  };
405
- //# sourceMappingURL=chunk-XA5U53VP.js.map
410
+ //# sourceMappingURL=chunk-LAYTHZN7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/referenceSourceUtils.ts","../src/core/referenceSourceAggregator.ts","../src/core/referenceListSource.ts","../src/core/referenceFilterCategories.ts","../src/core/index.ts"],"sourcesContent":["import type {\n NodeRef,\n ReferenceNode,\n SelectedReference\n} from \"../contracts/referenceSource.ts\";\nimport type { WorkspaceFileReference } from \"../contracts/index.ts\";\n\n/**\n * Reference Source Services 的共享 base 工具(形状处理,非取数)。\n * 设计见 docs/architecture/agent-reference-source-services.md §1 / §2。\n */\n\n/**\n * 「回到源根」哨兵 nodeId。源在 listSidebarGroups 里用它表示一个等价于源根的\n * 二级分组(如本地源的「个人」= 用户 home 根);picker 选中它走 navigateToRoot\n * 而非 ensureChildren,避免把哨兵当真实路径打后端。\n */\nexport const WORKSPACE_ROOT_GROUP_NODE_ID = \"__workspace_root__\";\n\n/** 节点的全局 key,用于 picker 树状态索引与去重。picker 不解析其内部结构。 */\nexport function nodeRefKey(ref: NodeRef): string {\n return `${ref.sourceId}\u0000${ref.nodeId}`;\n}\n\nexport function nodeKey(node: ReferenceNode): string {\n return nodeRefKey(node.ref);\n}\n\n/** 任意输入归一为 \"file\" | \"folder\"。 */\nexport function normalizeReferenceNodeKind(\n kind: string | null | undefined\n): \"file\" | \"folder\" {\n return kind === \"folder\" || kind === \"directory\" ? \"folder\" : \"file\";\n}\n\n/** folder 在前、同类按 displayName 本地化排序。 */\nexport function sortReferenceNodes(\n nodes: readonly ReferenceNode[]\n): ReferenceNode[] {\n return [...nodes].sort((left, right) => {\n if (left.kind !== right.kind) {\n return left.kind === \"folder\" ? -1 : 1;\n }\n return left.displayName.localeCompare(right.displayName, undefined, {\n sensitivity: \"base\"\n });\n });\n}\n\n/** 按 node key 去重,保留首次出现。支持 cursor append 的稳定去重。 */\nexport function dedupeReferenceNodes(\n nodes: readonly ReferenceNode[]\n): ReferenceNode[] {\n const seen = new Set<string>();\n const result: ReferenceNode[] = [];\n for (const node of nodes) {\n const key = nodeKey(node);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n result.push(node);\n }\n return result;\n}\n\n/** 续页累积:把新一页 append 到已有项后并去重(cursor 语义,不重排)。 */\nexport function appendReferencePage(\n current: readonly ReferenceNode[],\n next: readonly ReferenceNode[]\n): ReferenceNode[] {\n return dedupeReferenceNodes([...current, ...next]);\n}\n\n/** SelectedReference 与现有 WorkspaceFileReference 形状兼容,便于下游零改动复用。 */\nexport function selectedReferenceToWorkspaceFileReference(\n selected: SelectedReference\n): WorkspaceFileReference {\n return {\n path: selected.path,\n kind: selected.kind,\n ...(selected.displayName ? { displayName: selected.displayName } : {}),\n ...(selected.sourceId ? { sourceId: selected.sourceId } : {})\n };\n}\n","import type {\n ListChildrenResult,\n NodeRef,\n ReferenceNode,\n ReferencePreview,\n ReferenceScope,\n ReferenceSourceRegistry,\n ReferenceSourceService,\n SearchInput,\n SearchResult,\n SelectedReference\n} from \"../contracts/referenceSource.ts\";\n\n/**\n * 多源聚合器:根层级 = 每个可用源一个 folder 节点;其余按 node.ref.sourceId 委派。\n * 设计见 docs/architecture/agent-reference-source-services.md §3 / §4.4。\n */\n\n/**\n * 源根节点的 nodeId 哨兵。由聚合器创建源根节点时使用;\n * 任何真实源都不会产出 nodeId 等于此值的子节点(workspace 源 nodeId 是路径,\n * app 源 nodeId 以 \"app:\" 开头),因此路由无歧义。\n */\nexport const SOURCE_ROOT_NODE_ID = \"\u0000source-root\";\n\n/** 顶部 tab 的描述(source 身份 + 能力)。 */\nexport interface ReferenceSourceTab {\n sourceId: string;\n label: string;\n icon?: string;\n capabilities: ReferenceSourceService[\"capabilities\"];\n}\n\nexport interface ReferenceSourceAggregator {\n /** 顶部 tab 列表:每个可用源一个 tab(已过滤 isAvailable、按 order 排序)。 */\n listSources(scope: ReferenceScope): Promise<ReferenceSourceTab[]>;\n /** (备用,单树多根形态)根层级:每个可用源一个 folder 节点。 */\n listRoot(scope: ReferenceScope): Promise<ReferenceNode[]>;\n listChildren(\n scope: ReferenceScope,\n node: NodeRef,\n input?: {\n cursor?: string | null;\n filter?: string | null;\n signal?: AbortSignal;\n }\n ): Promise<ListChildrenResult>;\n search(\n scope: ReferenceScope,\n sourceId: string,\n input: SearchInput\n ): Promise<SearchResult>;\n open(scope: ReferenceScope, node: ReferenceNode): Promise<void>;\n readPreview(\n scope: ReferenceScope,\n node: ReferenceNode\n ): Promise<ReferencePreview | null>;\n resolveSelection(node: ReferenceNode): SelectedReference;\n /**\n * 把语义定位参数解析为某源内从根到目标分组的 NodeRef 路径(root → leaf)。\n * 源未实现 locateTarget / 未找到时返回 null。需先 listSources/listRoot 触发源加载。\n */\n locateTarget(\n scope: ReferenceScope,\n sourceId: string,\n params: Record<string, string>\n ): Promise<NodeRef[] | null>;\n /** 取已加载的源(用于读取 capabilities/metadata)。需先 listRoot 触发加载。 */\n getLoadedSource(sourceId: string): ReferenceSourceService | undefined;\n}\n\nexport function isSourceRootNode(ref: NodeRef): boolean {\n return ref.nodeId === SOURCE_ROOT_NODE_ID;\n}\n\nexport function createReferenceSourceAggregator(\n registry: ReferenceSourceRegistry\n): ReferenceSourceAggregator {\n // 同一 scope 内源稳定;缓存便于 resolveSelection 同步取源。\n const sourceMap = new Map<string, ReferenceSourceService>();\n\n async function loadSources(\n scope: ReferenceScope\n ): Promise<ReferenceSourceService[]> {\n const sources = await registry.getSources(scope);\n sourceMap.clear();\n for (const source of sources) {\n sourceMap.set(source.metadata.id, source);\n }\n return sources;\n }\n\n function requireSource(sourceId: string): ReferenceSourceService {\n const source = sourceMap.get(sourceId);\n if (!source) {\n throw new Error(`reference source not found: ${sourceId}`);\n }\n return source;\n }\n\n return {\n async listSources(scope) {\n const sources = await loadSources(scope);\n return sources.map((source) => ({\n sourceId: source.metadata.id,\n label: source.metadata.label,\n ...(source.metadata.icon ? { icon: source.metadata.icon } : {}),\n capabilities: source.capabilities\n }));\n },\n\n async listRoot(scope) {\n const sources = await loadSources(scope);\n return sources.map((source) => ({\n ref: { sourceId: source.metadata.id, nodeId: SOURCE_ROOT_NODE_ID },\n kind: \"folder\",\n displayName: source.metadata.label,\n hasChildren: true\n }));\n },\n\n async listChildren(scope, node, input) {\n const source = requireSource(node.sourceId);\n return source.listChildren(scope, {\n node: isSourceRootNode(node) ? null : node,\n cursor: input?.cursor ?? null,\n filter: input?.filter ?? null,\n signal: input?.signal\n });\n },\n\n async search(scope, sourceId, input) {\n const source = requireSource(sourceId);\n if (!source.search) {\n return { entries: [], nextCursor: null };\n }\n return source.search(scope, input);\n },\n\n async open(scope, node) {\n const source = requireSource(node.ref.sourceId);\n await source.open?.(scope, node);\n },\n\n async readPreview(scope, node) {\n const source = requireSource(node.ref.sourceId);\n return (await source.readPreview?.(scope, node)) ?? null;\n },\n\n resolveSelection(node) {\n const selected = requireSource(node.ref.sourceId).resolveSelection(node);\n return {\n ...selected,\n sourceId: selected.sourceId ?? node.ref.sourceId\n };\n },\n\n async locateTarget(scope, sourceId, params) {\n // 源可能尚未加载(getLoadedSource 依赖先前 listSources/listRoot);此处兜底加载一次。\n if (!sourceMap.has(sourceId)) {\n await loadSources(scope);\n }\n const source = sourceMap.get(sourceId);\n if (!source?.locateTarget) {\n return null;\n }\n return (await source.locateTarget(scope, params)) ?? null;\n },\n\n getLoadedSource(sourceId) {\n return sourceMap.get(sourceId);\n }\n };\n}\n\n/** 静态 registry:持有一组源,按 isAvailable 过滤、按 order 排序。 */\nexport function createStaticReferenceSourceRegistry(\n sources: readonly ReferenceSourceService[]\n): ReferenceSourceRegistry {\n return {\n async getSources(scope) {\n const availability = await Promise.all(\n sources.map(async (source) => {\n try {\n return await source.isAvailable(scope);\n } catch {\n return false;\n }\n })\n );\n return sources\n .filter((_, index) => availability[index])\n .sort((left, right) => left.metadata.order - right.metadata.order);\n }\n };\n}\n","import type {\n ListChildrenResult,\n NodeRef,\n ReferenceHandle,\n ReferenceNode,\n ReferencePreview,\n ReferenceScope,\n ReferenceSourceCapabilities,\n ReferenceSourceService,\n SearchInput,\n SearchResult,\n SelectedReference\n} from \"../contracts/referenceSource.ts\";\nimport type {\n WorkspaceFileReference,\n WorkspaceFileReferenceAdapter\n} from \"../contracts/index.ts\";\n\n/**\n * 统一的「引用列表协议」—— 与 app references 同一套 group/reference/parentGroupId/cursor 形状。\n * 各源(应用 / issue / 任务…)各自实现一个 ReferenceListBackend 把自家数据映射成此协议;\n * createReferenceListSource 把协议 item 映射成 ReferenceNode,各源前端逻辑完全复用。\n * 设计见 docs/architecture/agent-reference-source-services.md。\n */\n\nexport interface ReferenceListGroup {\n type: \"group\";\n /** 不透明分组 id;作为下钻的 parentGroupId 原样回传。 */\n id: string;\n displayName: string;\n /** 可选的展示上下文,用于详情/搜索副标题,避免 UI 泄露不透明 group id。 */\n parentLabel?: string | null;\n referenceCount?: number | null;\n /** 可选分组图标(data URL / 远程 URL),如应用产物源的 app 图标。 */\n iconUrl?: string | null;\n}\n\nexport interface ReferenceListFile {\n /** daemon/host 可解析的文件路径。 */\n path: string;\n displayName?: string | null;\n /**\n * 可选的归属标签(如应用名 / 议题标题),用于搜索结果副标题。\n * 各源在「跨整源搜索拍平」时填入父级上下文;不填则副标题回退展示 nodeId。\n */\n parentLabel?: string | null;\n sizeBytes?: number | null;\n mtimeMs?: number | null;\n mimeType?: string | null;\n}\n\nexport interface ReferenceListReference {\n type: \"reference\";\n reference: ReferenceListFile;\n}\n\nexport type ReferenceListItem = ReferenceListGroup | ReferenceListReference;\n\nexport interface ReferenceListRequest {\n /** null = 根层级。 */\n parentGroupId: string | null;\n cursor?: string | null;\n filter?: string | null;\n signal?: AbortSignal;\n}\n\nexport interface ReferenceListResult {\n items: ReferenceListItem[];\n nextCursor?: string | null;\n}\n\n/** 递归搜索请求(跨整源,非当前层 filter)。 */\nexport interface ReferenceListSearchRequest {\n query: string;\n /** 已选文件类型筛选分类 id(全局统一口径);空数组/缺省 = 不按类型过滤。 */\n filters?: string[];\n cursor?: string | null;\n limit?: number;\n signal?: AbortSignal;\n /**\n * 可选:把搜索限定在某个分组下(协议层不透明 group id,与 list 返回的 group.id 同形)。\n * 缺省/null = 跨整源搜索。由 createReferenceListSource 从 SearchInput.withinNodeId 解码而来。\n */\n withinGroupId?: string | null;\n}\n\n/** 各源自治的取数适配器:把自家数据映射成统一协议。 */\nexport interface ReferenceListBackend {\n list(\n scope: ReferenceScope,\n request: ReferenceListRequest\n ): Promise<ReferenceListResult>;\n /**\n * 可选:递归搜索。实现即代表该源支持全局搜索(对应 capabilities.searchable)。\n * 返回 flat reference items(协议层不返回 group)。\n */\n search?(\n scope: ReferenceScope,\n request: ReferenceListSearchRequest\n ): Promise<ReferenceListResult>;\n /**\n * 可选:把语义定位参数解析为从根到目标分组的「不透明分组 id」路径(root → leaf),\n * 用各源自家的 group id 方案(与 list 返回的 group.id 同形),wrapper 负责编成 NodeRef。\n * 返回 null 表示不支持或未找到。\n */\n locate?(\n scope: ReferenceScope,\n params: Record<string, string>\n ): Promise<string[] | null>;\n /**\n * 可选:把自家「不透明分组 id」解码成可被 agent 解析的领域句柄(见 ReferenceHandle)。\n * navigable 源实现;wrapper 负责先剥掉 GROUP_PREFIX 再调本方法。\n */\n describeHandle?(groupId: string): ReferenceHandle | null;\n}\n\nexport interface CreateReferenceListSourceInput {\n sourceId: string;\n label: string;\n order?: number;\n /** 可选源图标令牌(如 \"issue\"),供 picker 决定二级分组的兜底图标。 */\n icon?: string;\n capabilities: ReferenceSourceCapabilities;\n isAvailable: (scope: ReferenceScope) => boolean | Promise<boolean>;\n backend: ReferenceListBackend;\n /** open/preview 复用 host 链路(协议返回的 path 可被解析打开)。 */\n adapter: WorkspaceFileReferenceAdapter;\n}\n\nconst GROUP_PREFIX = \"g:\";\nconst FILE_PREFIX = \"f:\";\n\nexport function createReferenceListSource(\n input: CreateReferenceListSourceInput\n): ReferenceSourceService {\n const { sourceId, label, capabilities, isAvailable, backend, adapter } =\n input;\n\n function fileReferenceOf(node: ReferenceNode): WorkspaceFileReference {\n return { path: decodeSegment(FILE_PREFIX, node.ref.nodeId), kind: \"file\" };\n }\n\n const service: ReferenceSourceService = {\n metadata: {\n id: sourceId,\n label,\n order: input.order ?? 0,\n ...(input.icon ? { icon: input.icon } : {})\n },\n capabilities,\n isAvailable,\n\n async listChildren(\n scope: ReferenceScope,\n { node, cursor, filter }\n ): Promise<ListChildrenResult> {\n // file 节点没有子节点。\n if (node && node.nodeId.startsWith(FILE_PREFIX)) {\n return { entries: [], nextCursor: null };\n }\n const parentGroupId = node\n ? decodeSegment(GROUP_PREFIX, node.nodeId)\n : null;\n const result = await backend.list(scope, {\n parentGroupId,\n cursor: cursor ?? null,\n filter: filter ?? null\n });\n return {\n entries: result.items.map((item) => itemToNode(sourceId, item)),\n nextCursor: result.nextCursor ?? null\n };\n },\n\n async open(_scope: ReferenceScope, node: ReferenceNode): Promise<void> {\n await adapter.openReference?.(fileReferenceOf(node));\n },\n\n async readPreview(\n scope: ReferenceScope,\n node: ReferenceNode\n ): Promise<ReferencePreview | null> {\n if (!adapter.readReferencePreview) {\n return null;\n }\n return adapter.readReferencePreview({\n workspaceId: scope.workspaceId,\n reference: fileReferenceOf(node)\n });\n },\n\n resolveSelection(node: ReferenceNode): SelectedReference {\n return {\n path: decodeSegment(FILE_PREFIX, node.ref.nodeId),\n kind: \"file\",\n ...(node.displayName ? { displayName: node.displayName } : {})\n };\n }\n };\n\n // 仅当 backend 实现了定位时才暴露 locateTarget;把不透明分组 id 路径编成 NodeRef 路径。\n const backendLocate = backend.locate?.bind(backend);\n if (backendLocate) {\n service.locateTarget = async (\n scope: ReferenceScope,\n params: Record<string, string>\n ): Promise<NodeRef[] | null> => {\n const groupIds = await backendLocate(scope, params);\n if (!groupIds) {\n return null;\n }\n return groupIds.map((groupId) => ({\n sourceId,\n nodeId: GROUP_PREFIX + base64UrlEncode(groupId)\n }));\n };\n }\n\n // 仅当 backend 实现了句柄解码时才暴露 describeReferenceHandle:剥掉 GROUP_PREFIX\n // 得到 backend 自家分组 id,交给 backend 解码成领域句柄。非分组节点(文件/源根)→ null。\n const backendDescribeHandle = backend.describeHandle?.bind(backend);\n if (backendDescribeHandle) {\n service.describeReferenceHandle = (\n node: ReferenceNode\n ): ReferenceHandle | null => {\n if (!node.ref.nodeId.startsWith(GROUP_PREFIX)) {\n return null;\n }\n return backendDescribeHandle(\n decodeSegment(GROUP_PREFIX, node.ref.nodeId)\n );\n };\n }\n\n // 仅当 backend 实现了递归搜索时才暴露 search,与 capabilities.searchable 保持一致。\n const backendSearch = backend.search?.bind(backend);\n if (backendSearch) {\n service.search = async (\n scope: ReferenceScope,\n input: SearchInput\n ): Promise<SearchResult> => {\n // 把左栏选中的分组节点(GROUP_PREFIX nodeId)解码成协议层不透明 group id;\n // 非分组(源根 / 文件节点 / 缺省)→ null,退回跨整源搜索。\n const withinGroupId =\n input.withinNodeId && input.withinNodeId.startsWith(GROUP_PREFIX)\n ? decodeSegment(GROUP_PREFIX, input.withinNodeId)\n : null;\n const result = await backendSearch(scope, {\n query: input.query,\n cursor: input.cursor ?? null,\n ...(input.filters && input.filters.length > 0\n ? { filters: input.filters }\n : {}),\n ...(input.limit == null ? {} : { limit: input.limit }),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(withinGroupId == null ? {} : { withinGroupId })\n });\n return {\n entries: result.items.map((item) => itemToNode(sourceId, item)),\n nextCursor: result.nextCursor ?? null\n };\n };\n }\n\n return service;\n}\n\nfunction itemToNode(sourceId: string, item: ReferenceListItem): ReferenceNode {\n if (item.type === \"group\") {\n return {\n ref: { sourceId, nodeId: GROUP_PREFIX + base64UrlEncode(item.id) },\n kind: \"folder\",\n displayName: item.displayName,\n hasChildren: true,\n ...(item.parentLabel?.trim()\n ? { contextLabel: item.parentLabel.trim() }\n : {}),\n ...(item.referenceCount == null\n ? {}\n : { childCount: item.referenceCount }),\n ...(item.iconUrl ? { iconUrl: item.iconUrl } : {})\n };\n }\n const reference = item.reference;\n return {\n ref: { sourceId, nodeId: FILE_PREFIX + base64UrlEncode(reference.path) },\n kind: \"file\",\n displayName: reference.displayName?.trim() || basename(reference.path),\n ...(reference.parentLabel?.trim()\n ? { contextLabel: reference.parentLabel.trim() }\n : {}),\n ...(reference.sizeBytes == null ? {} : { sizeBytes: reference.sizeBytes }),\n ...(reference.mtimeMs == null ? {} : { mtimeMs: reference.mtimeMs }),\n ...(reference.mimeType ? { mimeType: reference.mimeType } : {})\n };\n}\n\nfunction decodeSegment(prefix: string, nodeId: string): string {\n if (!nodeId.startsWith(prefix)) {\n throw new Error(\n `reference-list nodeId missing ${prefix} prefix: ${nodeId}`\n );\n }\n return base64UrlDecode(nodeId.slice(prefix.length));\n}\n\nfunction basename(path: string): string {\n const trimmed = path.replace(/\\/+$/, \"\");\n const index = trimmed.lastIndexOf(\"/\");\n return index >= 0 ? trimmed.slice(index + 1) : trimmed;\n}\n\n/** UTF-8 安全的 base64url(浏览器/Node 通用)。 */\nexport function base64UrlEncode(value: string): string {\n const bytes = new TextEncoder().encode(value);\n let binary = \"\";\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport function base64UrlDecode(value: string): string {\n const normalized = value.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const binary = atob(normalized);\n const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));\n return new TextDecoder().decode(bytes);\n}\n","/**\n * 全局统一「文件类型筛选分类」—— 筛选与搜索在底层是同一能力:search() 接收的\n * filters 即此处的分类 id 数组,各源 daemon 按扩展名/媒体类型真正过滤。\n *\n * 这是分类口径的「单一 TS 来源」。其余各处均为逐字镜像,改一处务必同步全部:\n * - Go 宿主侧:services/tuttid/data/workspace/reference_filter_categories.go\n * - group-chat 应用:apps/server/src/domains/reference-filter-categories.ts\n * - vibe-design 应用:server/src/routes/references-routes.ts\n * - ai-media-canvas 应用:apps/server/src/http/references.ts\n * - app_factory 技能契约:app_factory_reference/references/manifest-contract.md\n *\n * 分类口径:image(图片)/ video(视频)/ document(文档,含表格)/ webpage(网页)/ other(其他)。\n * 音频、代码、压缩包等未单列扩展名,统一归入 \"other\" 兜底。\n */\n\nexport type ReferenceFilterCategoryId =\n | \"image\"\n | \"video\"\n | \"document\"\n | \"webpage\"\n | \"other\";\n\nexport interface ReferenceFilterCategory {\n id: ReferenceFilterCategoryId;\n /** 展示名走 i18n:referencePicker 命名空间下的 copy key,由 UI 层解析。 */\n labelKey: string;\n /** 该分类归属的文件扩展名(小写,不含点)。\"other\" 为空,表示「未收录/无扩展名」兜底。 */\n extensions: readonly string[];\n}\n\n/**\n * 分类定义(展示顺序即数组顺序)。每个分类按文件扩展名归类;\n * 文件夹不参与类型过滤(始终保留以便继续下钻)。\n */\nexport const REFERENCE_FILTER_CATEGORIES: readonly ReferenceFilterCategory[] = [\n {\n id: \"image\",\n labelKey: \"referencePicker.fileTypeImage\",\n extensions: [\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"gif\",\n \"webp\",\n \"svg\",\n \"bmp\",\n \"ico\",\n \"heic\"\n ]\n },\n {\n id: \"video\",\n labelKey: \"referencePicker.fileTypeVideo\",\n extensions: [\"mp4\", \"mov\", \"avi\", \"mkv\", \"webm\"]\n },\n {\n id: \"document\",\n labelKey: \"referencePicker.fileTypeDocument\",\n extensions: [\n \"pdf\",\n \"doc\",\n \"docx\",\n \"txt\",\n \"md\",\n \"markdown\",\n \"rtf\",\n \"odt\",\n \"pages\",\n \"key\",\n \"ppt\",\n \"pptx\",\n \"xls\",\n \"xlsx\",\n \"csv\",\n \"tsv\",\n \"numbers\"\n ]\n },\n {\n id: \"webpage\",\n labelKey: \"referencePicker.fileTypeWebpage\",\n extensions: [\"html\", \"htm\", \"mhtml\", \"url\", \"webloc\"]\n },\n { id: \"other\", labelKey: \"referencePicker.fileTypeOther\", extensions: [] }\n];\n\nconst CATEGORY_BY_EXTENSION: ReadonlyMap<string, ReferenceFilterCategoryId> =\n new Map(\n REFERENCE_FILTER_CATEGORIES.flatMap((category) =>\n category.extensions.map((ext) => [ext, category.id] as const)\n )\n );\n\n/** 从文件名末段推断分类;无扩展名或未收录的扩展名归入「other」。 */\nexport function categoryOfFileName(name: string): ReferenceFilterCategoryId {\n const dotIndex = name.lastIndexOf(\".\");\n if (dotIndex <= 0 || dotIndex === name.length - 1) {\n return \"other\";\n }\n const ext = name.slice(dotIndex + 1).toLowerCase();\n return CATEGORY_BY_EXTENSION.get(ext) ?? \"other\";\n}\n\n/**\n * 分类筛选判定:空 ids = 不筛选(全部通过);文件夹始终通过(保留导航能力);\n * 仅对文件按其分类匹配。供需要在客户端兜底过滤的调用方复用。\n */\nexport function matchesFilterCategories(\n name: string,\n isFolder: boolean,\n filterIds: readonly string[]\n): boolean {\n if (filterIds.length === 0 || isFolder) {\n return true;\n }\n return filterIds.includes(categoryOfFileName(name));\n}\n","import type { WorkspaceFileReference } from \"../contracts/index.ts\";\n\nexport * from \"./referenceSourceUtils.ts\";\nexport * from \"./referenceSourceAggregator.ts\";\nexport * from \"./referenceListSource.ts\";\nexport * from \"./referenceFilterCategories.ts\";\n\nexport function uniqueWorkspaceFileReferences(\n refs: readonly WorkspaceFileReference[]\n): WorkspaceFileReference[] {\n const unique = new Map<string, WorkspaceFileReference>();\n for (const ref of refs) {\n const normalizedPath = ref.path.trim();\n if (!normalizedPath || unique.has(normalizedPath)) {\n continue;\n }\n unique.set(normalizedPath, {\n displayName: ref.displayName?.trim() || undefined,\n kind: ref.kind === \"folder\" ? \"folder\" : \"file\",\n path: normalizedPath,\n ...(ref.mtimeMs === undefined ? {} : { mtimeMs: ref.mtimeMs }),\n ...(ref.sizeBytes === undefined ? {} : { sizeBytes: ref.sizeBytes })\n });\n }\n return [...unique.values()];\n}\n"],"mappings":"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","names":["input"]}
@@ -115,6 +115,8 @@ interface SelectedReference {
115
115
  path: string;
116
116
  kind: "file" | "folder";
117
117
  displayName?: string;
118
+ /** 保留引用来源,供上层区分 workspace 文件与 host 本地文件等同形 path。 */
119
+ sourceId?: string;
118
120
  }
119
121
  interface ReferenceSourceMetadata {
120
122
  id: string;
@@ -179,6 +181,7 @@ interface WorkspaceFileReference {
179
181
  mtimeMs?: number | null;
180
182
  path: string;
181
183
  sizeBytes?: number | null;
184
+ sourceId?: string;
182
185
  }
183
186
  interface WorkspaceFileReferenceDirectoryListing {
184
187
  directoryPath: string;
@@ -18,7 +18,7 @@ import {
18
18
  selectedReferenceToWorkspaceFileReference,
19
19
  sortReferenceNodes,
20
20
  uniqueWorkspaceFileReferences
21
- } from "../chunk-XA5U53VP.js";
21
+ } from "../chunk-LAYTHZN7.js";
22
22
  export {
23
23
  REFERENCE_FILTER_CATEGORIES,
24
24
  SOURCE_ROOT_NODE_ID,
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  selectedReferenceToWorkspaceFileReference,
19
19
  sortReferenceNodes,
20
20
  uniqueWorkspaceFileReferences
21
- } from "./chunk-XA5U53VP.js";
21
+ } from "./chunk-LAYTHZN7.js";
22
22
  export {
23
23
  REFERENCE_FILTER_CATEGORIES,
24
24
  SOURCE_ROOT_NODE_ID,
@@ -9,8 +9,8 @@ import {
9
9
  prefetchReferenceTree,
10
10
  useWorkspaceFileReferencePickerView,
11
11
  workspaceFileReferenceDefaultExpandedDepth
12
- } from "../chunk-C3G4ADEJ.js";
13
- import "../chunk-XA5U53VP.js";
12
+ } from "../chunk-CT42WDOK.js";
13
+ import "../chunk-LAYTHZN7.js";
14
14
  export {
15
15
  ROOT_CHILDREN_KEY,
16
16
  collectVisibleTreeEntries,
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { WorkspaceFileReferenceCopy, WorkspaceFileReferenceAdapter, WorkspaceFileReference, ReferenceHandle, ReferenceLocateTarget } from '../contracts/index.js';
2
+ import { WorkspaceFileReferenceCopy, WorkspaceFileReferenceAdapter, WorkspaceFileReference, ReferenceHandle, ReferenceLocateTarget, ReferenceNode } from '../contracts/index.js';
3
3
  import { JSX } from 'react';
4
4
  import { R as ReferenceSourceAggregator } from '../referenceSourceAggregator-B8ATnJOZ.js';
5
5
 
@@ -45,6 +45,7 @@ interface ReferenceSourcePickerProps {
45
45
  copy: WorkspaceFileReferenceCopy;
46
46
  /** 可选:打开时直达某事项/应用分组(展开并聚焦)。 */
47
47
  initialTarget?: ReferenceLocateTarget | null;
48
+ isNodeSelectable?: (node: ReferenceNode) => boolean;
48
49
  onClose: () => void;
49
50
  onConfirm: (refs: WorkspaceFileReference[]) => void;
50
51
  /**
@@ -55,6 +56,6 @@ interface ReferenceSourcePickerProps {
55
56
  open: boolean;
56
57
  workspaceId: string;
57
58
  }
58
- declare function ReferenceSourcePicker({ aggregator, copy, initialTarget, onClose, onConfirm, onConfirmBundles, open, workspaceId }: ReferenceSourcePickerProps): JSX.Element | null;
59
+ declare function ReferenceSourcePicker({ aggregator, copy, initialTarget, isNodeSelectable, onClose, onConfirm, onConfirmBundles, open, workspaceId }: ReferenceSourcePickerProps): JSX.Element | null;
59
60
 
60
61
  export { type ReferenceBundleSelection, type ReferenceGroupedSelection, ReferenceSourcePicker, type ReferenceSourcePickerProps, WorkspaceFileReferencePicker, type WorkspaceFileReferencePickerProps };
package/dist/ui/index.js CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  createReferenceSourcePickerController,
4
4
  normalizeDirectoryPath,
5
5
  useWorkspaceFileReferencePickerView
6
- } from "../chunk-C3G4ADEJ.js";
6
+ } from "../chunk-CT42WDOK.js";
7
7
  import {
8
8
  REFERENCE_FILTER_CATEGORIES,
9
9
  WORKSPACE_ROOT_GROUP_NODE_ID,
10
10
  nodeRefKey,
11
11
  selectedReferenceToWorkspaceFileReference
12
- } from "../chunk-XA5U53VP.js";
12
+ } from "../chunk-LAYTHZN7.js";
13
13
 
14
14
  // src/ui/internal/reference/WorkspaceFileReferencePicker.tsx
15
15
  import {
@@ -757,6 +757,7 @@ function useReferenceSourcePickerView({
757
757
  initialTarget = null,
758
758
  onClose,
759
759
  onConfirm,
760
+ isNodeSelectable,
760
761
  onConfirmBundles
761
762
  }) {
762
763
  const readSnapshot = useSnapshot;
@@ -1027,16 +1028,28 @@ function useReferenceSourcePickerView({
1027
1028
  [controller, snapshot.activeSourceId, navigateToRoot]
1028
1029
  );
1029
1030
  const isSelected = useCallback(
1030
- (node) => snapshot.selection.some(
1031
+ (node) => (isNodeSelectable?.(node) ?? true) && snapshot.selection.some(
1031
1032
  (item) => nodeRefKey(item.ref) === nodeRefKey(node.ref)
1032
1033
  ),
1033
- [snapshot.selection]
1034
+ [isNodeSelectable, snapshot.selection]
1035
+ );
1036
+ const isSelectable = useCallback(
1037
+ (node) => isNodeSelectable?.(node) ?? true,
1038
+ [isNodeSelectable]
1039
+ );
1040
+ const selectableSelection = useMemo(
1041
+ () => snapshot.selection.filter(isSelectable),
1042
+ [isSelectable, snapshot.selection]
1034
1043
  );
1035
1044
  const [isConfirming, setIsConfirming] = useState3(false);
1036
1045
  const confirm = useCallback(async () => {
1037
1046
  if (isConfirming) {
1038
1047
  return;
1039
1048
  }
1049
+ if (selectableSelection.length === 0) {
1050
+ controller.clearSelection();
1051
+ return;
1052
+ }
1040
1053
  setIsConfirming(true);
1041
1054
  try {
1042
1055
  if (onConfirmBundles) {
@@ -1061,7 +1074,14 @@ function useReferenceSourcePickerView({
1061
1074
  } finally {
1062
1075
  setIsConfirming(false);
1063
1076
  }
1064
- }, [controller, isConfirming, onClose, onConfirm, onConfirmBundles]);
1077
+ }, [
1078
+ controller,
1079
+ isConfirming,
1080
+ onClose,
1081
+ onConfirm,
1082
+ onConfirmBundles,
1083
+ selectableSelection.length
1084
+ ]);
1065
1085
  const [previewState, setPreviewState] = useState3({
1066
1086
  status: "empty"
1067
1087
  });
@@ -1205,8 +1225,8 @@ function useReferenceSourcePickerView({
1205
1225
  // 底部「加载更多」在途(查询态 = 增长重查;浏览态 = cursor append)。
1206
1226
  isLoadingMore: isQuery ? activeTabState?.isSearchLoadingMore ?? false : currentChildren?.loading ?? false,
1207
1227
  focusedNode,
1208
- selection: snapshot.selection,
1209
- selectionCount: snapshot.selection.length,
1228
+ selection: selectableSelection,
1229
+ selectionCount: selectableSelection.length,
1210
1230
  setActiveSource,
1211
1231
  enterFolder,
1212
1232
  selectGroup,
@@ -1215,9 +1235,23 @@ function useReferenceSourcePickerView({
1215
1235
  setFocusedNode,
1216
1236
  setSearchQuery: (query) => controller.setSearchQuery(query, searchScopeNodeId),
1217
1237
  setFilters: (filters) => controller.setSearchFilters(filters, searchScopeNodeId),
1218
- toggleSelection: (node) => controller.toggleSelection(node),
1219
- toggleSingleSelectionAndExpand: (node) => controller.toggleSingleSelectionAndExpand(node),
1238
+ toggleSelection: (node) => {
1239
+ if (isSelectable(node)) {
1240
+ controller.toggleSelection(node);
1241
+ }
1242
+ },
1243
+ toggleSingleSelectionAndExpand: (node) => {
1244
+ if (isSelectable(node)) {
1245
+ controller.toggleSingleSelectionAndExpand(node);
1246
+ return;
1247
+ }
1248
+ controller.clearSelection();
1249
+ if (node.kind === "folder") {
1250
+ controller.toggleNode(node);
1251
+ }
1252
+ },
1220
1253
  loadMore: () => isQuery ? controller.loadMoreSearch() : controller.loadMore(currentNode),
1254
+ isSelectable,
1221
1255
  isSelected,
1222
1256
  confirm,
1223
1257
  isConfirming
@@ -1274,6 +1308,7 @@ function ReferenceSourcePicker({
1274
1308
  aggregator,
1275
1309
  copy,
1276
1310
  initialTarget,
1311
+ isNodeSelectable,
1277
1312
  onClose,
1278
1313
  onConfirm,
1279
1314
  onConfirmBundles,
@@ -1287,6 +1322,7 @@ function ReferenceSourcePicker({
1287
1322
  open,
1288
1323
  workspaceRootGroupLabel: copy.t("referencePicker.workspaceRootGroup"),
1289
1324
  initialTarget,
1325
+ isNodeSelectable,
1290
1326
  onClose,
1291
1327
  onConfirm,
1292
1328
  onConfirmBundles
@@ -1461,6 +1497,7 @@ function ReferenceSourcePicker({
1461
1497
  node,
1462
1498
  selected: view.isSelected(node),
1463
1499
  onFocus: view.setFocusedNode,
1500
+ selectable: view.isSelectable(node),
1464
1501
  onSingleSelect: view.toggleSingleSelectionAndExpand,
1465
1502
  onToggle: view.toggleSelection
1466
1503
  },
@@ -1687,18 +1724,20 @@ function SearchResultRow({
1687
1724
  node,
1688
1725
  focused,
1689
1726
  selected,
1727
+ selectable,
1690
1728
  onFocus,
1691
1729
  onSingleSelect,
1692
1730
  onToggle
1693
1731
  }) {
1694
1732
  const isFolder = node.kind === "folder";
1695
1733
  const contextLabel = node.contextLabel ?? node.ref.nodeId;
1734
+ const active = selected || focused && selectable;
1696
1735
  return /* @__PURE__ */ jsxs4(
1697
1736
  "div",
1698
1737
  {
1699
1738
  className: cn3(
1700
1739
  "grid cursor-pointer grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-[6px] border py-2.5 pr-1 pl-3 transition-colors",
1701
- focused || selected ? "border-border bg-transparency-block" : "border-transparent bg-transparent hover:border-border/70 hover:bg-transparency-block"
1740
+ active ? "border-border bg-transparency-block" : "border-transparent bg-transparent hover:border-border/70 hover:bg-transparency-block"
1702
1741
  ),
1703
1742
  onClick: () => {
1704
1743
  onFocus(node);
@@ -1712,7 +1751,7 @@ function SearchResultRow({
1712
1751
  /* @__PURE__ */ jsx4(FullTextTooltip, { content: contextLabel, children: /* @__PURE__ */ jsx4("span", { className: "block truncate text-[11px] text-[var(--text-secondary)]", children: contextLabel }) })
1713
1752
  ] })
1714
1753
  ] }),
1715
- /* @__PURE__ */ jsx4(
1754
+ selectable ? /* @__PURE__ */ jsx4(
1716
1755
  Button3,
1717
1756
  {
1718
1757
  "aria-label": node.displayName,
@@ -1727,7 +1766,7 @@ function SearchResultRow({
1727
1766
  },
1728
1767
  children: selected ? /* @__PURE__ */ jsx4(CheckIcon2, { size: 14 }) : /* @__PURE__ */ jsx4(AddLinedIcon2, { className: "text-[var(--text-secondary)]", size: 16 })
1729
1768
  }
1730
- )
1769
+ ) : null
1731
1770
  ]
1732
1771
  }
1733
1772
  );
@@ -2129,7 +2168,9 @@ function TreeNodeRow({
2129
2168
  const childState = view.childrenByKey[key];
2130
2169
  const childEntries = view.sortNodes(childState?.entries ?? []);
2131
2170
  const selected = view.isSelected(node);
2171
+ const selectable = view.isSelectable(node);
2132
2172
  const focused = isFocused(view.focusedNode, node);
2173
+ const active = selected || focused && selectable;
2133
2174
  const [shouldRenderChildContent, setShouldRenderChildContent] = useState4(expanded);
2134
2175
  useEffect3(() => {
2135
2176
  if (expanded) {
@@ -2175,7 +2216,7 @@ function TreeNodeRow({
2175
2216
  {
2176
2217
  className: cn3(
2177
2218
  "flex cursor-pointer items-center gap-2 rounded-[6px] py-1.5 pr-1 transition-colors",
2178
- focused || selected ? "bg-transparency-block" : "hover:bg-transparency-block"
2219
+ active ? "bg-transparency-block" : "hover:bg-transparency-block"
2179
2220
  ),
2180
2221
  style: { paddingLeft: `${depth * TREE_INDENT + 8}px` },
2181
2222
  onClick: () => {
@@ -2214,7 +2255,7 @@ function TreeNodeRow({
2214
2255
  children: node.displayName
2215
2256
  }
2216
2257
  ) }),
2217
- /* @__PURE__ */ jsx4(
2258
+ selectable ? /* @__PURE__ */ jsx4(
2218
2259
  Button3,
2219
2260
  {
2220
2261
  "aria-label": node.displayName,
@@ -2228,9 +2269,15 @@ function TreeNodeRow({
2228
2269
  view.setFocusedNode(node);
2229
2270
  view.toggleSelection(node);
2230
2271
  },
2231
- children: selected ? /* @__PURE__ */ jsx4(CheckIcon2, { size: 14 }) : /* @__PURE__ */ jsx4(AddLinedIcon2, { className: "text-[var(--text-secondary)]", size: 16 })
2272
+ children: selected ? /* @__PURE__ */ jsx4(CheckIcon2, { size: 14 }) : /* @__PURE__ */ jsx4(
2273
+ AddLinedIcon2,
2274
+ {
2275
+ className: "text-[var(--text-secondary)]",
2276
+ size: 16
2277
+ }
2278
+ )
2232
2279
  }
2233
- )
2280
+ ) : null
2234
2281
  ]
2235
2282
  }
2236
2283
  ),