@tutti-os/workspace-file-reference 0.0.14 → 0.0.15

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.
package/dist/ui/index.js CHANGED
@@ -3,23 +3,20 @@ import {
3
3
  createReferenceSourcePickerController,
4
4
  normalizeDirectoryPath,
5
5
  useWorkspaceFileReferencePickerView
6
- } from "../chunk-CD3YNHYJ.js";
6
+ } from "../chunk-KSK6PILG.js";
7
7
  import {
8
+ REFERENCE_FILTER_CATEGORIES,
8
9
  WORKSPACE_ROOT_GROUP_NODE_ID,
9
10
  nodeRefKey,
10
11
  selectedReferenceToWorkspaceFileReference
11
- } from "../chunk-7C3RZYQE.js";
12
+ } from "../chunk-HRC2MZ5H.js";
12
13
 
13
14
  // src/ui/internal/reference/WorkspaceFileReferencePicker.tsx
14
- import { useId } from "react";
15
- import { createPortal } from "react-dom";
16
15
  import {
17
- Button as Button3,
18
- Card,
19
- CardContent,
20
- CardHeader,
21
- CardTitle,
22
- CloseIcon,
16
+ Dialog,
17
+ DialogContent,
18
+ DialogHeader,
19
+ DialogTitle,
23
20
  cn as cn2
24
21
  } from "@tutti-os/ui-system";
25
22
 
@@ -160,6 +157,12 @@ function WorkspaceFileReferencePickerTreeEntry({
160
157
  }, workspaceFileReferenceTreeCollapseDurationMs);
161
158
  return () => window.clearTimeout(timeoutId);
162
159
  }, [expanded]);
160
+ const handleRowSelect = () => {
161
+ onFocusPath(entry.path);
162
+ if (isFolder) {
163
+ onToggleFolder(entry);
164
+ }
165
+ };
163
166
  const shouldBuildChildContent = expanded || shouldRenderChildContent;
164
167
  const childContent = shouldBuildChildContent ? childState?.loading ? /* @__PURE__ */ jsxs(
165
168
  "div",
@@ -202,14 +205,24 @@ function WorkspaceFileReferencePickerTreeEntry({
202
205
  /* @__PURE__ */ jsxs(
203
206
  "div",
204
207
  {
208
+ "aria-current": focused ? "true" : void 0,
205
209
  className: cn(
206
- "flex items-center gap-2 rounded-[6px] py-1.5 pr-1 transition-colors",
210
+ "flex cursor-pointer items-center gap-2 rounded-[6px] py-1.5 pr-1 transition-colors select-none",
207
211
  "nodrag [-webkit-app-region:no-drag]",
208
212
  focused || selected ? "bg-transparency-block" : "hover:bg-transparency-block"
209
213
  ),
214
+ role: "button",
215
+ tabIndex: 0,
210
216
  style: {
211
217
  paddingLeft: `${(childDepth - 1) * workspaceFileReferenceTreeIndent + 8}px`
212
218
  },
219
+ onClick: handleRowSelect,
220
+ onKeyDown: (event) => {
221
+ if (event.key === "Enter" || event.key === " ") {
222
+ event.preventDefault();
223
+ handleRowSelect();
224
+ }
225
+ },
213
226
  children: [
214
227
  isFolder ? /* @__PURE__ */ jsx(
215
228
  "button",
@@ -232,24 +245,10 @@ function WorkspaceFileReferencePickerTreeEntry({
232
245
  )
233
246
  }
234
247
  ) : null,
235
- /* @__PURE__ */ jsxs(
236
- "button",
237
- {
238
- className: "nodrag flex min-w-0 flex-1 items-center gap-2 text-left [-webkit-app-region:no-drag]",
239
- type: "button",
240
- onClick: (event) => {
241
- event.stopPropagation();
242
- onFocusPath(entry.path);
243
- if (entry.kind === "folder") {
244
- onToggleFolder(entry);
245
- }
246
- },
247
- children: [
248
- isFolder ? /* @__PURE__ */ jsx(FolderFilledIcon, { className: "size-4 shrink-0 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx(FileIcon, { className: "size-4 shrink-0 text-[var(--text-tertiary)]" }),
249
- /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] text-[var(--text-primary)]", children: resolveWorkspaceFileReferenceLabel(entry) })
250
- ]
251
- }
252
- ),
248
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [
249
+ isFolder ? /* @__PURE__ */ jsx(FolderFilledIcon, { className: "size-4 shrink-0 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx(FileIcon, { className: "size-4 shrink-0 text-[var(--text-tertiary)]" }),
250
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[13px] text-[var(--text-primary)]", children: resolveWorkspaceFileReferenceLabel(entry) })
251
+ ] }),
253
252
  /* @__PURE__ */ jsx(
254
253
  Button,
255
254
  {
@@ -305,30 +304,29 @@ function WorkspaceFileReferencePickerSearchEntry({
305
304
  return /* @__PURE__ */ jsxs(
306
305
  "div",
307
306
  {
307
+ "aria-current": focused ? "true" : void 0,
308
308
  className: cn(
309
- "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-[6px] border py-2.5 pr-1 pl-3 transition-colors",
309
+ "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 select-none",
310
310
  "nodrag [-webkit-app-region:no-drag]",
311
311
  focused || selected ? "border-border bg-transparency-block" : "border-transparent bg-transparent hover:border-border/70 hover:bg-transparency-block"
312
312
  ),
313
+ role: "button",
314
+ tabIndex: 0,
315
+ onClick: () => onFocusPath(entry.path),
316
+ onKeyDown: (event) => {
317
+ if (event.key === "Enter" || event.key === " ") {
318
+ event.preventDefault();
319
+ onFocusPath(entry.path);
320
+ }
321
+ },
313
322
  children: [
314
- /* @__PURE__ */ jsxs(
315
- "button",
316
- {
317
- className: "nodrag flex min-w-0 items-center gap-3 text-left [-webkit-app-region:no-drag]",
318
- type: "button",
319
- onClick: (event) => {
320
- event.stopPropagation();
321
- onFocusPath(entry.path);
322
- },
323
- children: [
324
- /* @__PURE__ */ jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-[var(--transparency-block)] text-[var(--text-tertiary)]", children: isFolder ? /* @__PURE__ */ jsx(FolderFilledIcon, { className: "size-4 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx(FileIcon, { className: "size-4 text-[var(--text-tertiary)]" }) }),
325
- /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
326
- /* @__PURE__ */ jsx("span", { className: "block truncate text-[13px] font-medium text-[var(--text-primary)]", children: resolveWorkspaceFileReferenceLabel(entry) }),
327
- /* @__PURE__ */ jsx("span", { className: "block truncate text-[11px] text-[var(--text-secondary)]", children: entry.path })
328
- ] })
329
- ]
330
- }
331
- ),
323
+ /* @__PURE__ */ jsxs("div", { className: "nodrag flex min-w-0 items-center gap-3 text-left [-webkit-app-region:no-drag]", children: [
324
+ /* @__PURE__ */ jsx("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-[var(--transparency-block)] text-[var(--text-tertiary)]", children: isFolder ? /* @__PURE__ */ jsx(FolderFilledIcon, { className: "size-4 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx(FileIcon, { className: "size-4 text-[var(--text-tertiary)]" }) }),
325
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
326
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-[13px] font-medium text-[var(--text-primary)]", children: resolveWorkspaceFileReferenceLabel(entry) }),
327
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-[11px] text-[var(--text-secondary)]", children: entry.path })
328
+ ] })
329
+ ] }),
332
330
  /* @__PURE__ */ jsx(
333
331
  Button,
334
332
  {
@@ -546,8 +544,6 @@ function WorkspaceFileReferencePickerFeedback2({
546
544
 
547
545
  // src/ui/internal/reference/WorkspaceFileReferencePicker.tsx
548
546
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
549
- var workspaceFileReferencePickerBackdropMotionClassName = "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-[180ms] motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none";
550
- var workspaceFileReferencePickerPanelMotionClassName = "motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-[0.96] motion-safe:duration-[250ms] motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none";
551
547
  function WorkspaceFileReferencePicker({
552
548
  copy,
553
549
  fileAdapter,
@@ -555,9 +551,9 @@ function WorkspaceFileReferencePicker({
555
551
  onClose,
556
552
  onConfirm,
557
553
  open,
554
+ scoped = false,
558
555
  workspaceId
559
556
  }) {
560
- const titleId = useId();
561
557
  const {
562
558
  browseRootEntries,
563
559
  directoryStateByPath,
@@ -582,52 +578,27 @@ function WorkspaceFileReferencePicker({
582
578
  open,
583
579
  workspaceId
584
580
  });
585
- if (!open) {
586
- return null;
587
- }
588
- const dialog = /* @__PURE__ */ jsx3(
589
- "div",
581
+ return /* @__PURE__ */ jsx3(
582
+ Dialog,
590
583
  {
591
- className: cn2(
592
- "nodrag fixed inset-0 grid place-items-center bg-[var(--backdrop)] px-3 py-4 backdrop-blur-md [-webkit-app-region:no-drag] sm:px-6 sm:py-8",
593
- workspaceFileReferencePickerBackdropMotionClassName
594
- ),
595
- style: { zIndex: "var(--z-panel)" },
596
- onClick: onClose,
584
+ open,
585
+ onOpenChange: (nextOpen) => {
586
+ if (!nextOpen) {
587
+ onClose();
588
+ }
589
+ },
597
590
  children: /* @__PURE__ */ jsxs3(
598
- Card,
591
+ DialogContent,
599
592
  {
600
- "aria-labelledby": titleId,
601
- "aria-modal": "true",
593
+ "aria-describedby": void 0,
602
594
  className: cn2(
603
- "nodrag flex h-[min(88vh,44rem)] w-full max-w-5xl origin-center flex-col gap-0 overflow-hidden border-[var(--line-1)] bg-[var(--background-fronted)] py-0 text-[var(--text-primary)] shadow-panel [-webkit-app-region:no-drag] sm:h-[min(82vh,44rem)]",
604
- workspaceFileReferencePickerPanelMotionClassName
595
+ "nodrag flex h-[min(88vh,44rem)] w-full max-w-5xl flex-col gap-0 overflow-hidden border-[var(--line-1)] bg-[var(--background-fronted)] p-0 text-[var(--text-primary)] shadow-panel [-webkit-app-region:no-drag] sm:h-[min(82vh,44rem)] sm:max-w-5xl"
605
596
  ),
606
- role: "dialog",
607
- onClick: (event) => event.stopPropagation(),
597
+ overlayClassName: cn2("nodrag", scoped && "!absolute"),
598
+ portaled: !scoped,
608
599
  children: [
609
- /* @__PURE__ */ jsx3(
610
- CardHeader,
611
- {
612
- className: "border-b px-4 py-4 sm:px-6 sm:py-5",
613
- style: { borderBottomColor: "var(--line-1)" },
614
- children: /* @__PURE__ */ jsxs3("div", { className: "flex items-start justify-between gap-4", children: [
615
- /* @__PURE__ */ jsx3("div", { className: "min-w-0", children: /* @__PURE__ */ jsx3(CardTitle, { id: titleId, children: copy.t("referencePicker.title") }) }),
616
- /* @__PURE__ */ jsx3(
617
- Button3,
618
- {
619
- "aria-label": copy.t("actions.cancel"),
620
- size: "icon-sm",
621
- type: "button",
622
- variant: "ghost",
623
- onClick: onClose,
624
- children: /* @__PURE__ */ jsx3(CloseIcon, { size: 16 })
625
- }
626
- )
627
- ] })
628
- }
629
- ),
630
- /* @__PURE__ */ jsxs3(CardContent, { className: "grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 lg:grid-cols-[minmax(0,1.15fr)_minmax(20rem,0.85fr)] lg:grid-rows-1", children: [
600
+ /* @__PURE__ */ jsx3(DialogHeader, { className: "flex-none border-b border-[var(--line-1)] px-4 py-4 sm:px-6 sm:py-5", children: /* @__PURE__ */ jsx3(DialogTitle, { children: copy.t("referencePicker.title") }) }),
601
+ /* @__PURE__ */ jsxs3("div", { className: "grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] gap-0 overflow-hidden p-0 lg:grid-cols-[minmax(0,1.15fr)_minmax(20rem,0.85fr)] lg:grid-rows-1", children: [
631
602
  /* @__PURE__ */ jsx3(
632
603
  WorkspaceFileReferencePickerBrowserPane,
633
604
  {
@@ -671,33 +642,31 @@ function WorkspaceFileReferencePicker({
671
642
  )
672
643
  }
673
644
  );
674
- if (typeof document === "undefined") {
675
- return dialog;
676
- }
677
- return createPortal(dialog, document.body);
678
645
  }
679
646
 
680
647
  // src/ui/internal/reference/ReferenceSourcePicker.tsx
681
648
  import {
682
649
  useEffect as useEffect3,
683
- useId as useId2,
650
+ useId,
684
651
  useRef as useRef2,
685
652
  useState as useState3
686
653
  } from "react";
687
- import { createPortal as createPortal2 } from "react-dom";
654
+ import { createPortal } from "react-dom";
688
655
  import {
689
656
  ArrowRightIcon as ArrowRightIcon2,
690
657
  Badge as Badge2,
691
- Button as Button4,
692
- Card as Card2,
693
- CardContent as CardContent2,
694
- CardHeader as CardHeader2,
695
- CardTitle as CardTitle2,
658
+ Button as Button3,
659
+ Card,
660
+ CardContent,
661
+ CardHeader,
662
+ CardTitle,
696
663
  CheckIcon as CheckIcon2,
697
- CloseIcon as CloseIcon2,
664
+ ChevronDownIcon,
665
+ CloseIcon,
698
666
  FileIcon as FileIcon3,
699
667
  FolderFilledIcon as FolderFilledIcon3,
700
668
  Input as Input2,
669
+ IssueIcon,
701
670
  ResizableHandle,
702
671
  ResizablePanel,
703
672
  ResizablePanelGroup,
@@ -707,6 +676,9 @@ import {
707
676
  cn as cn3
708
677
  } from "@tutti-os/ui-system";
709
678
  import { AddIcon } from "@tutti-os/ui-system/icons";
679
+ import {
680
+ WorkspaceFilePreviewSurface
681
+ } from "@tutti-os/workspace-file-preview/react";
710
682
 
711
683
  // src/react/internal/reference/useReferenceSourcePickerView.ts
712
684
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
@@ -714,13 +686,18 @@ import { useSnapshot } from "valtio";
714
686
  import {
715
687
  sortWorkspaceFileEntriesForArrangeMode
716
688
  } from "@tutti-os/workspace-file-manager/services";
717
- var WORKSPACE_ROOT_GROUP_LABEL = "\u5DE5\u4F5C\u533A";
689
+ import {
690
+ createWorkspaceFilePreviewLoadedState
691
+ } from "@tutti-os/workspace-file-preview";
718
692
  function useReferenceSourcePickerView({
719
693
  aggregator,
720
694
  workspaceId,
721
695
  open,
696
+ workspaceRootGroupLabel,
697
+ initialTarget = null,
722
698
  onClose,
723
- onConfirm
699
+ onConfirm,
700
+ onConfirmBundles
724
701
  }) {
725
702
  const readSnapshot = useSnapshot;
726
703
  const scope = useMemo(() => ({ workspaceId }), [workspaceId]);
@@ -732,8 +709,6 @@ function useReferenceSourcePickerView({
732
709
  const [breadcrumbBySource, setBreadcrumbBySource] = useState2({});
733
710
  const [focusedNode, setFocusedNode] = useState2(null);
734
711
  const [arrangeMode, setArrangeMode] = useState2("none");
735
- const [expandedSources, setExpandedSources] = useState2({});
736
- const expandSeededRef = useRef(false);
737
712
  const sortNodes = useCallback(
738
713
  (nodes) => {
739
714
  if (arrangeMode === "none") {
@@ -757,6 +732,7 @@ function useReferenceSourcePickerView({
757
732
  [arrangeMode]
758
733
  );
759
734
  const autoEnteredSourcesRef = useRef(/* @__PURE__ */ new Set());
735
+ const appliedInitialTargetRef = useRef(null);
760
736
  useEffect2(() => {
761
737
  if (!open) {
762
738
  return;
@@ -765,13 +741,44 @@ function useReferenceSourcePickerView({
765
741
  controller.open();
766
742
  setBreadcrumbBySource({});
767
743
  setFocusedNode(null);
768
- setExpandedSources({});
769
- expandSeededRef.current = false;
770
744
  autoEnteredSourcesRef.current = /* @__PURE__ */ new Set();
745
+ appliedInitialTargetRef.current = null;
771
746
  return () => {
772
747
  controller.close();
773
748
  };
774
749
  }, [controller, open]);
750
+ useEffect2(() => {
751
+ const target = initialTarget;
752
+ if (!open || !target || appliedInitialTargetRef.current === target) {
753
+ return;
754
+ }
755
+ appliedInitialTargetRef.current = target;
756
+ autoEnteredSourcesRef.current.add(target.sourceId);
757
+ let canceled = false;
758
+ void controller.locatePath(target).then((path) => {
759
+ if (canceled) {
760
+ return;
761
+ }
762
+ const group = path[0];
763
+ if (!group) {
764
+ autoEnteredSourcesRef.current.delete(target.sourceId);
765
+ return;
766
+ }
767
+ controller.setActiveSource(target.sourceId);
768
+ setBreadcrumbBySource((current) => ({
769
+ ...current,
770
+ [group.ref.sourceId]: [group]
771
+ }));
772
+ controller.ensureChildren(group);
773
+ const deepest = path[path.length - 1];
774
+ setFocusedNode(path.length > 1 && deepest ? deepest : null);
775
+ }).catch(() => {
776
+ autoEnteredSourcesRef.current.delete(target.sourceId);
777
+ });
778
+ return () => {
779
+ canceled = true;
780
+ };
781
+ }, [open, initialTarget, controller]);
775
782
  const activeSourceId = snapshot.activeSourceId;
776
783
  const activeTab = useMemo(
777
784
  () => snapshot.tabs.find((tab) => tab.sourceId === activeSourceId) ?? null,
@@ -782,7 +789,8 @@ function useReferenceSourcePickerView({
782
789
  const currentNode = breadcrumb.at(-1) ?? null;
783
790
  const currentKey = currentNode ? nodeRefKey(currentNode.ref) : ROOT_CHILDREN_KEY;
784
791
  const activeTabState = activeSourceId ? snapshot.bySource[activeSourceId] : void 0;
785
- const isSearch = activeTabState?.mode === "search" && activeTabState.searchQuery.trim() !== "";
792
+ const activeFilters = activeTabState?.searchFilters ?? [];
793
+ const isQuery = activeTabState?.mode === "search" && (activeTabState.searchQuery.trim() !== "" || activeFilters.length > 0);
786
794
  const currentChildren = activeTabState?.childrenByKey[currentKey];
787
795
  const currentEntries = useMemo(
788
796
  () => sortNodes(currentChildren?.entries ?? []),
@@ -811,18 +819,51 @@ function useReferenceSourcePickerView({
811
819
  const workspaceRoot = {
812
820
  ref: { sourceId, nodeId: WORKSPACE_ROOT_GROUP_NODE_ID },
813
821
  kind: "folder",
814
- displayName: WORKSPACE_ROOT_GROUP_LABEL
822
+ displayName: workspaceRootGroupLabel
815
823
  };
816
824
  result[sourceId] = [workspaceRoot, ...folders];
817
825
  }
818
826
  }
819
827
  return result;
828
+ }, [
829
+ snapshot.tabs,
830
+ snapshot.bySource,
831
+ aggregator,
832
+ scope,
833
+ workspaceRootGroupLabel
834
+ ]);
835
+ const sidebarHasMoreBySource = useMemo(() => {
836
+ const result = {};
837
+ for (const tab of snapshot.tabs) {
838
+ const sourceId = tab.sourceId;
839
+ const provided = aggregator.getLoadedSource(sourceId)?.listSidebarGroups?.(scope);
840
+ if (provided && provided.length > 0) {
841
+ result[sourceId] = false;
842
+ continue;
843
+ }
844
+ const root = snapshot.bySource[sourceId]?.childrenByKey[ROOT_CHILDREN_KEY];
845
+ result[sourceId] = Boolean(root?.nextCursor);
846
+ }
847
+ return result;
820
848
  }, [snapshot.tabs, snapshot.bySource, aggregator, scope]);
849
+ const sidebarLoadingMoreBySource = useMemo(() => {
850
+ const result = {};
851
+ for (const tab of snapshot.tabs) {
852
+ const root = snapshot.bySource[tab.sourceId]?.childrenByKey[ROOT_CHILDREN_KEY];
853
+ result[tab.sourceId] = Boolean(root?.loaded && root.loading);
854
+ }
855
+ return result;
856
+ }, [snapshot.tabs, snapshot.bySource]);
821
857
  const sidebarGroups = activeSourceId ? sidebarGroupsBySource[activeSourceId] ?? [] : [];
822
- const selectedGroupKey = currentNode != null ? nodeRefKey(currentNode.ref) : activeSourceId && !capabilities?.navigable ? nodeRefKey({
858
+ const rootGroupNode = breadcrumb[0] ?? null;
859
+ const searchScopeNodeId = rootGroupNode ? rootGroupNode.ref.nodeId : null;
860
+ const selectedGroupKey = rootGroupNode ? nodeRefKey(rootGroupNode.ref) : activeSourceId && !capabilities?.navigable ? nodeRefKey({
823
861
  sourceId: activeSourceId,
824
862
  nodeId: WORKSPACE_ROOT_GROUP_NODE_ID
825
863
  }) : null;
864
+ useEffect2(() => {
865
+ controller.setSearchScope(searchScopeNodeId);
866
+ }, [controller, searchScopeNodeId]);
826
867
  const setActiveSource = useCallback(
827
868
  (sourceId) => {
828
869
  controller.setActiveSource(sourceId);
@@ -850,7 +891,7 @@ function useReferenceSourcePickerView({
850
891
  [controller]
851
892
  );
852
893
  useEffect2(() => {
853
- if (!open || !activeSourceId || !capabilities?.navigable) {
894
+ if (!open || !activeSourceId) {
854
895
  return;
855
896
  }
856
897
  if (autoEnteredSourcesRef.current.has(activeSourceId)) {
@@ -867,23 +908,15 @@ function useReferenceSourcePickerView({
867
908
  }
868
909
  autoEnteredSourcesRef.current.add(activeSourceId);
869
910
  enterFolder(firstGroup);
870
- }, [
871
- open,
872
- activeSourceId,
873
- capabilities?.navigable,
874
- sidebarGroups,
875
- breadcrumbBySource,
876
- enterFolder
877
- ]);
911
+ }, [open, activeSourceId, sidebarGroups, breadcrumbBySource, enterFolder]);
878
912
  useEffect2(() => {
879
- if (!open || expandSeededRef.current || !activeSourceId) {
913
+ if (!open) {
880
914
  return;
881
915
  }
882
- expandSeededRef.current = true;
883
- setExpandedSources(
884
- (current) => current[activeSourceId] != null ? current : { ...current, [activeSourceId]: true }
885
- );
886
- }, [open, activeSourceId]);
916
+ for (const tab of snapshot.tabs) {
917
+ controller.ensureSourceRoot(tab.sourceId);
918
+ }
919
+ }, [open, snapshot.tabs, controller]);
887
920
  const navigateToBreadcrumb = useCallback(
888
921
  (index) => {
889
922
  if (!activeSourceId) {
@@ -924,26 +957,11 @@ function useReferenceSourcePickerView({
924
957
  navigateToRoot(sourceId);
925
958
  return;
926
959
  }
927
- enterFolder(node);
928
- },
929
- [controller, snapshot.activeSourceId, enterFolder, navigateToRoot]
930
- );
931
- const isSourceExpanded = useCallback(
932
- (sourceId) => expandedSources[sourceId] ?? false,
933
- [expandedSources]
934
- );
935
- const toggleSourceExpanded = useCallback(
936
- (sourceId) => {
937
- const willExpand = !(expandedSources[sourceId] ?? false);
938
- setExpandedSources((current) => ({
939
- ...current,
940
- [sourceId]: !(current[sourceId] ?? false)
941
- }));
942
- if (willExpand && !aggregator.getLoadedSource(sourceId)?.listSidebarGroups) {
943
- controller.ensureSourceRoot(sourceId);
944
- }
960
+ controller.ensureChildren(node);
961
+ setBreadcrumbBySource((current) => ({ ...current, [sourceId]: [node] }));
962
+ setFocusedNode(null);
945
963
  },
946
- [aggregator, controller, expandedSources]
964
+ [controller, snapshot.activeSourceId, navigateToRoot]
947
965
  );
948
966
  const isSelected = useCallback(
949
967
  (node) => snapshot.selection.some(
@@ -958,16 +976,122 @@ function useReferenceSourcePickerView({
958
976
  }
959
977
  setIsConfirming(true);
960
978
  try {
961
- const selected = await controller.confirm();
962
- onConfirm(selected.map(selectedReferenceToWorkspaceFileReference));
979
+ if (onConfirmBundles) {
980
+ const grouped = await controller.confirmGrouped();
981
+ onConfirmBundles({
982
+ files: grouped.files.map(selectedReferenceToWorkspaceFileReference),
983
+ bundles: grouped.bundles.map((bundle) => ({
984
+ sourceId: bundle.root.ref.sourceId,
985
+ nodeId: bundle.root.ref.nodeId,
986
+ displayName: bundle.root.displayName,
987
+ iconUrl: bundle.root.iconUrl ?? null,
988
+ files: bundle.files.map(selectedReferenceToWorkspaceFileReference)
989
+ }))
990
+ });
991
+ } else {
992
+ const selected = await controller.confirm();
993
+ onConfirm(selected.map(selectedReferenceToWorkspaceFileReference));
994
+ }
963
995
  onClose();
964
996
  } finally {
965
997
  setIsConfirming(false);
966
998
  }
967
- }, [controller, isConfirming, onClose, onConfirm]);
999
+ }, [controller, isConfirming, onClose, onConfirm, onConfirmBundles]);
1000
+ const [previewState, setPreviewState] = useState2({
1001
+ status: "empty"
1002
+ });
1003
+ const previewObjectUrlRef = useRef(null);
1004
+ const revokePreviewObjectUrl = useCallback(() => {
1005
+ if (previewObjectUrlRef.current) {
1006
+ URL.revokeObjectURL(previewObjectUrlRef.current);
1007
+ previewObjectUrlRef.current = null;
1008
+ }
1009
+ }, []);
1010
+ useEffect2(() => {
1011
+ const node = focusedNode;
1012
+ if (!node) {
1013
+ revokePreviewObjectUrl();
1014
+ setPreviewState({ status: "empty" });
1015
+ return;
1016
+ }
1017
+ if (node.kind === "folder") {
1018
+ revokePreviewObjectUrl();
1019
+ setPreviewState({ node, status: "directory" });
1020
+ return;
1021
+ }
1022
+ const previewable = aggregator.getLoadedSource(node.ref.sourceId)?.capabilities.previewable ?? false;
1023
+ if (!previewable) {
1024
+ revokePreviewObjectUrl();
1025
+ setPreviewState({ node, status: "unsupported" });
1026
+ return;
1027
+ }
1028
+ let cancelled = false;
1029
+ revokePreviewObjectUrl();
1030
+ setPreviewState({ node, status: "loading" });
1031
+ void (async () => {
1032
+ try {
1033
+ const preview = await aggregator.readPreview(scope, node);
1034
+ if (cancelled) {
1035
+ return;
1036
+ }
1037
+ if (!preview) {
1038
+ setPreviewState({ node, status: "unsupported" });
1039
+ return;
1040
+ }
1041
+ const loaded = createWorkspaceFilePreviewLoadedState({
1042
+ bytes: preview.bytes,
1043
+ contentType: preview.contentType,
1044
+ entry: {
1045
+ kind: node.kind,
1046
+ name: node.displayName,
1047
+ path: node.ref.nodeId,
1048
+ mtimeMs: node.mtimeMs ?? null,
1049
+ sizeBytes: node.sizeBytes ?? null
1050
+ },
1051
+ target: {
1052
+ fileKind: preview.kind,
1053
+ name: node.displayName,
1054
+ path: node.ref.nodeId,
1055
+ mtimeMs: node.mtimeMs ?? null,
1056
+ sizeBytes: node.sizeBytes ?? null
1057
+ }
1058
+ });
1059
+ if (cancelled) {
1060
+ return;
1061
+ }
1062
+ if (loaded.status === "image") {
1063
+ const objectUrl = URL.createObjectURL(
1064
+ new Blob([loaded.bytes], { type: loaded.contentType })
1065
+ );
1066
+ previewObjectUrlRef.current = objectUrl;
1067
+ setPreviewState({ node, objectUrl, status: "image" });
1068
+ return;
1069
+ }
1070
+ if (loaded.status === "text") {
1071
+ setPreviewState({ content: loaded.content, node, status: "text" });
1072
+ return;
1073
+ }
1074
+ setPreviewState({
1075
+ node,
1076
+ reason: loaded.reason,
1077
+ ...loaded.maxSizeBytes == null ? {} : { maxSizeBytes: loaded.maxSizeBytes },
1078
+ status: "readonly"
1079
+ });
1080
+ } catch {
1081
+ if (!cancelled) {
1082
+ setPreviewState({ node, status: "error" });
1083
+ }
1084
+ }
1085
+ })();
1086
+ return () => {
1087
+ cancelled = true;
1088
+ };
1089
+ }, [aggregator, focusedNode, revokePreviewObjectUrl, scope]);
1090
+ useEffect2(() => revokePreviewObjectUrl, [revokePreviewObjectUrl]);
968
1091
  return {
969
1092
  tabs: snapshot.tabs,
970
1093
  activeSourceId,
1094
+ previewState,
971
1095
  activeTabLabel: activeTab?.label ?? "",
972
1096
  capabilities,
973
1097
  // 内容区递归就地树:当前选中二级节点的子条目(本地根时为源根子条目)。
@@ -983,15 +1107,25 @@ function useReferenceSourcePickerView({
983
1107
  currentNode,
984
1108
  sidebarGroups,
985
1109
  sidebarGroupsBySource,
986
- isSourceExpanded,
987
- toggleSourceExpanded,
1110
+ sidebarHasMoreBySource,
1111
+ sidebarLoadingMoreBySource,
1112
+ loadMoreSidebarGroups: (sourceId) => controller.loadMoreSourceRoot(sourceId),
988
1113
  selectedGroupKey,
989
1114
  arrangeMode,
990
1115
  setArrangeMode,
991
- isSearch,
1116
+ // 查询态(关键词或筛选任一非空)→ 平铺结果;否则浏览树。
1117
+ isQuery,
992
1118
  searchQuery: activeTabState?.searchQuery ?? "",
993
- isLoading: isSearch ? activeTabState?.isSearchLoading ?? false : currentChildren?.loading ?? false,
994
- hasMore: !isSearch && Boolean(currentChildren?.nextCursor),
1119
+ // 当前源支持的文件类型筛选分类(不支持则空数组,picker 据此决定是否展示筛选下拉)。
1120
+ filterCategories: capabilities?.filterable ? REFERENCE_FILTER_CATEGORIES : [],
1121
+ activeFilters,
1122
+ // 搜索态:仅在「还没有任何结果」时显示 spinner;细化关键词(已有结果)时
1123
+ // 保留旧结果直到新结果就绪,避免内容区在 spinner/结果间反复切换造成闪烁。
1124
+ isLoading: isQuery ? (activeTabState?.isSearchLoading ?? false) && (activeTabState?.searchEntries.length ?? 0) === 0 : currentChildren?.loading ?? false,
1125
+ // 查询态:增长式分页是否还有更多;浏览态:cursor 是否有下一页。
1126
+ hasMore: isQuery ? activeTabState?.searchHasMore ?? false : Boolean(currentChildren?.nextCursor),
1127
+ // 底部「加载更多」在途(查询态 = 增长重查;浏览态 = cursor append)。
1128
+ isLoadingMore: isQuery ? activeTabState?.isSearchLoadingMore ?? false : currentChildren?.loading ?? false,
995
1129
  focusedNode,
996
1130
  selection: snapshot.selection,
997
1131
  selectionCount: snapshot.selection.length,
@@ -1001,9 +1135,10 @@ function useReferenceSourcePickerView({
1001
1135
  navigateToBreadcrumb,
1002
1136
  navigateToRoot,
1003
1137
  setFocusedNode,
1004
- setSearchQuery: (query) => controller.setSearchQuery(query),
1138
+ setSearchQuery: (query) => controller.setSearchQuery(query, searchScopeNodeId),
1139
+ setFilters: (filters) => controller.setSearchFilters(filters, searchScopeNodeId),
1005
1140
  toggleSelection: (node) => controller.toggleSelection(node),
1006
- loadMore: () => controller.loadMore(currentNode),
1141
+ loadMore: () => isQuery ? controller.loadMoreSearch() : controller.loadMore(currentNode),
1007
1142
  isSelected,
1008
1143
  confirm,
1009
1144
  isConfirming
@@ -1012,17 +1147,7 @@ function useReferenceSourcePickerView({
1012
1147
 
1013
1148
  // src/ui/internal/reference/ReferenceSourcePicker.tsx
1014
1149
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1015
- var L = {
1016
- sourceColumn: "\u5206\u7C7B",
1017
- selectGroupHint: "\u4ECE\u5DE6\u4FA7\u9009\u62E9\u4E00\u4E2A\u76EE\u5F55",
1018
- previewSource: "\u4EA7\u51FA\u6765\u6E90",
1019
- previewModified: "\u4EA7\u51FA\u65F6\u95F4",
1020
- previewSize: "\u6587\u4EF6\u5927\u5C0F",
1021
- previewHierarchy: "\u6240\u5C5E\u5C42\u7EA7",
1022
- reference: "\u5F15\u7528",
1023
- loadMore: "\u52A0\u8F7D\u66F4\u591A",
1024
- emptyPreview: "\u9009\u62E9\u4E00\u4E2A\u6587\u4EF6\u67E5\u770B\u8BE6\u60C5"
1025
- };
1150
+ var SIDEBAR_GROUP_PAGE_SIZE = 20;
1026
1151
  function autoFitPanelWidth(groupEl, contentEl, panel, trailingPx) {
1027
1152
  if (!groupEl || !contentEl || !panel) {
1028
1153
  return;
@@ -1051,19 +1176,35 @@ function autoFitPanelWidth(groupEl, contentEl, panel, trailingPx) {
1051
1176
  function ReferenceSourcePicker({
1052
1177
  aggregator,
1053
1178
  copy,
1179
+ initialTarget,
1054
1180
  onClose,
1055
1181
  onConfirm,
1182
+ onConfirmBundles,
1056
1183
  open,
1057
1184
  workspaceId
1058
1185
  }) {
1059
- const titleId = useId2();
1186
+ const titleId = useId();
1060
1187
  const view = useReferenceSourcePickerView({
1061
1188
  aggregator,
1062
1189
  workspaceId,
1063
1190
  open,
1191
+ workspaceRootGroupLabel: copy.t("referencePicker.workspaceRootGroup"),
1192
+ initialTarget,
1064
1193
  onClose,
1065
- onConfirm
1194
+ onConfirm,
1195
+ onConfirmBundles
1066
1196
  });
1197
+ const activeFilterSet = new Set(view.activeFilters);
1198
+ const toggleFilter = (id) => {
1199
+ const next = new Set(activeFilterSet);
1200
+ if (next.has(id)) {
1201
+ next.delete(id);
1202
+ } else {
1203
+ next.add(id);
1204
+ }
1205
+ view.setFilters([...next]);
1206
+ };
1207
+ const clearFilters = () => view.setFilters([]);
1067
1208
  const layoutRef = useRef2(null);
1068
1209
  const sidebarContentRef = useRef2(null);
1069
1210
  const middleContentRef = useRef2(null);
@@ -1092,7 +1233,7 @@ function ReferenceSourcePicker({
1092
1233
  style: { zIndex: "var(--z-panel)" },
1093
1234
  onClick: onClose,
1094
1235
  children: /* @__PURE__ */ jsxs4(
1095
- Card2,
1236
+ Card,
1096
1237
  {
1097
1238
  "aria-labelledby": titleId,
1098
1239
  "aria-modal": "true",
@@ -1100,38 +1241,45 @@ function ReferenceSourcePicker({
1100
1241
  role: "dialog",
1101
1242
  onClick: (event) => event.stopPropagation(),
1102
1243
  children: [
1103
- /* @__PURE__ */ jsx4(CardHeader2, { className: "gap-3 px-4 pt-4 pb-4 sm:px-6", children: /* @__PURE__ */ jsxs4("div", { className: "flex items-start justify-between gap-4", children: [
1104
- /* @__PURE__ */ jsx4(CardTitle2, { id: titleId, children: copy.t("referencePicker.title") }),
1244
+ /* @__PURE__ */ jsx4(CardHeader, { className: "gap-3 px-4 pt-4 pb-4 sm:px-6", children: /* @__PURE__ */ jsxs4("div", { className: "flex items-start justify-between gap-4", children: [
1245
+ /* @__PURE__ */ jsx4(CardTitle, { id: titleId, children: copy.t("referencePicker.title") }),
1105
1246
  /* @__PURE__ */ jsx4(
1106
- Button4,
1247
+ Button3,
1107
1248
  {
1108
1249
  "aria-label": copy.t("actions.cancel"),
1109
1250
  size: "icon-sm",
1110
1251
  type: "button",
1111
1252
  variant: "ghost",
1112
1253
  onClick: onClose,
1113
- children: /* @__PURE__ */ jsx4(CloseIcon2, { size: 16 })
1254
+ children: /* @__PURE__ */ jsx4(CloseIcon, { size: 16 })
1114
1255
  }
1115
1256
  )
1116
1257
  ] }) }),
1117
- /* @__PURE__ */ jsx4(CardContent2, { className: "flex min-h-0 flex-1 overflow-hidden border-t border-[var(--line-1)] p-0", children: /* @__PURE__ */ jsx4("div", { ref: layoutRef, className: "flex min-h-0 flex-1", children: /* @__PURE__ */ jsxs4(
1258
+ /* @__PURE__ */ jsx4(CardContent, { className: "flex min-h-0 flex-1 overflow-hidden border-t border-[var(--line-1)] p-0", children: /* @__PURE__ */ jsx4("div", { ref: layoutRef, className: "flex min-h-0 min-w-0 flex-1", children: /* @__PURE__ */ jsxs4(
1118
1259
  ResizablePanelGroup,
1119
1260
  {
1120
- className: "min-h-0 flex-1",
1261
+ className: "min-h-0 min-w-0 flex-1",
1121
1262
  orientation: "horizontal",
1122
- defaultLayout: { sidebar: 2, middle: 5, preview: 3 },
1263
+ defaultLayout: { sidebar: 2.5, middle: 4.5, preview: 3 },
1123
1264
  children: [
1124
1265
  /* @__PURE__ */ jsx4(
1125
1266
  ResizablePanel,
1126
1267
  {
1127
1268
  id: "sidebar",
1128
- className: "min-h-0 border-r border-[var(--line-1)]",
1129
- defaultSize: 20,
1130
- minSize: "180px",
1269
+ className: "min-h-0 min-w-0",
1270
+ defaultSize: 15,
1271
+ minSize: "150px",
1131
1272
  panelRef: (handle) => {
1132
1273
  sidebarPanelRef.current = handle;
1133
1274
  },
1134
- children: /* @__PURE__ */ jsx4(SourceSidebar, { contentRef: sidebarContentRef, view })
1275
+ children: /* @__PURE__ */ jsx4(
1276
+ SourceSidebar,
1277
+ {
1278
+ contentRef: sidebarContentRef,
1279
+ copy,
1280
+ view
1281
+ }
1282
+ )
1135
1283
  }
1136
1284
  ),
1137
1285
  /* @__PURE__ */ jsx4(
@@ -1139,6 +1287,7 @@ function ReferenceSourcePicker({
1139
1287
  {
1140
1288
  disableDoubleClick: true,
1141
1289
  withHandle: true,
1290
+ className: "after:bg-[var(--line-1)]",
1142
1291
  onDoubleClick: fitSidebar
1143
1292
  }
1144
1293
  ),
@@ -1146,73 +1295,105 @@ function ReferenceSourcePicker({
1146
1295
  ResizablePanel,
1147
1296
  {
1148
1297
  id: "middle",
1149
- className: "min-h-0",
1298
+ className: "min-h-0 min-w-0",
1150
1299
  defaultSize: 50,
1151
1300
  minSize: "260px",
1152
1301
  panelRef: (handle) => {
1153
1302
  middlePanelRef.current = handle;
1154
1303
  },
1155
1304
  children: /* @__PURE__ */ jsxs4("div", { className: "flex h-full min-h-0 flex-col", children: [
1156
- /* @__PURE__ */ jsx4("div", { className: "flex items-center gap-2 border-b border-[var(--line-1)] p-3", children: /* @__PURE__ */ jsxs4("div", { className: "relative flex-1", children: [
1157
- /* @__PURE__ */ jsx4(SearchIcon2, { className: "pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--text-tertiary)]" }),
1158
- /* @__PURE__ */ jsx4(
1159
- Input2,
1305
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 border-b border-[var(--line-1)] p-3", children: [
1306
+ /* @__PURE__ */ jsxs4("div", { className: "relative flex-1", children: [
1307
+ /* @__PURE__ */ jsx4(SearchIcon2, { className: "pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-[var(--text-tertiary)]" }),
1308
+ /* @__PURE__ */ jsx4(
1309
+ Input2,
1310
+ {
1311
+ className: "pl-9",
1312
+ placeholder: copy.t(
1313
+ "referencePicker.searchPlaceholder"
1314
+ ),
1315
+ value: view.searchQuery,
1316
+ onChange: (event) => view.setSearchQuery(event.target.value)
1317
+ }
1318
+ )
1319
+ ] }),
1320
+ view.capabilities?.filterable && view.filterCategories.length > 0 ? /* @__PURE__ */ jsx4(
1321
+ FilterCategoryFilter,
1160
1322
  {
1161
- className: "pl-9",
1162
- placeholder: copy.t(
1163
- "referencePicker.searchPlaceholder"
1164
- ),
1165
- value: view.searchQuery,
1166
- onChange: (event) => view.setSearchQuery(event.target.value)
1323
+ categories: view.filterCategories,
1324
+ copy,
1325
+ selected: activeFilterSet,
1326
+ onClear: clearFilters,
1327
+ onToggle: toggleFilter
1167
1328
  }
1168
- )
1169
- ] }) }),
1170
- /* @__PURE__ */ jsx4(ScrollArea2, { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxs4(
1171
- "div",
1329
+ ) : null
1330
+ ] }),
1331
+ /* @__PURE__ */ jsx4(
1332
+ ScrollArea2,
1172
1333
  {
1173
- ref: middleContentRef,
1174
- className: "flex flex-col gap-[2px] p-3",
1175
- children: [
1176
- view.isLoading ? /* @__PURE__ */ jsx4(Feedback, { children: /* @__PURE__ */ jsx4(Spinner2, { size: 16 }) }) : view.isSearch ? (
1177
- // 搜索:扁平结果
1178
- view.searchResults.length === 0 ? /* @__PURE__ */ jsx4(Feedback, { children: copy.t("referencePicker.emptyDirectory") }) : view.searchResults.map((node) => /* @__PURE__ */ jsx4(
1179
- SearchResultRow,
1180
- {
1181
- focused: isFocused(view.focusedNode, node),
1182
- node,
1183
- selected: view.isSelected(node),
1184
- onFocus: view.setFocusedNode,
1185
- onToggle: view.toggleSelection
1186
- },
1187
- nodeRefKey(node.ref)
1188
- ))
1189
- ) : !hasSelectedGroup ? /* @__PURE__ */ jsx4(Feedback, { children: L.selectGroupHint }) : view.currentEntries.length === 0 ? /* @__PURE__ */ jsx4(Feedback, { children: copy.t("referencePicker.emptyDirectory") }) : (
1190
- // 浏览:就地递归展开树(复刻 agent 引用面板文件树交互)
1191
- view.currentEntries.map((node) => /* @__PURE__ */ jsx4(
1192
- TreeNodeRow,
1193
- {
1194
- copy,
1195
- depth: 0,
1196
- node,
1197
- view
1198
- },
1199
- nodeRefKey(node.ref)
1200
- ))
1201
- ),
1202
- view.hasMore && hasSelectedGroup && !view.isSearch ? /* @__PURE__ */ jsx4(
1203
- Button4,
1204
- {
1205
- className: "mt-1 w-full",
1206
- size: "sm",
1207
- type: "button",
1208
- variant: "ghost",
1209
- onClick: view.loadMore,
1210
- children: L.loadMore
1334
+ className: "min-h-0 flex-1",
1335
+ viewportProps: {
1336
+ // 拉到底部(距底 <120px)自动加载更多 —— 查询态走增长式分页,
1337
+ // 浏览态走 cursor 续页。已在加载/无更多时由 loadMore 内部 no-op。
1338
+ onScroll: (event) => {
1339
+ const el = event.currentTarget;
1340
+ if (view.hasMore && !view.isLoading && !view.isLoadingMore && el.scrollHeight - el.scrollTop - el.clientHeight < 120) {
1341
+ view.loadMore();
1211
1342
  }
1212
- ) : null
1213
- ]
1343
+ }
1344
+ },
1345
+ children: /* @__PURE__ */ jsxs4(
1346
+ "div",
1347
+ {
1348
+ ref: middleContentRef,
1349
+ className: "flex flex-col gap-[2px] p-3",
1350
+ children: [
1351
+ view.isLoading ? /* @__PURE__ */ jsx4(Feedback, { children: /* @__PURE__ */ jsx4(Spinner2, { size: 16 }) }) : view.isQuery ? (
1352
+ // 查询态(关键词或筛选):扁平结果
1353
+ view.searchResults.length === 0 ? /* @__PURE__ */ jsx4(Feedback, { children: copy.t("referencePicker.emptySearch") }) : view.searchResults.map((node) => /* @__PURE__ */ jsx4(
1354
+ SearchResultRow,
1355
+ {
1356
+ focused: isFocused(view.focusedNode, node),
1357
+ node,
1358
+ selected: view.isSelected(node),
1359
+ onFocus: view.setFocusedNode,
1360
+ onToggle: view.toggleSelection
1361
+ },
1362
+ nodeRefKey(node.ref)
1363
+ ))
1364
+ ) : !hasSelectedGroup ? /* @__PURE__ */ jsx4(Feedback, { children: copy.t("referencePicker.selectGroupHint") }) : view.currentEntries.length === 0 ? /* @__PURE__ */ jsx4(Feedback, { children: copy.t("referencePicker.emptyDirectory") }) : (
1365
+ // 浏览:就地递归展开树(复刻 agent 引用面板文件树交互)
1366
+ view.currentEntries.map((node) => /* @__PURE__ */ jsx4(
1367
+ TreeNodeRow,
1368
+ {
1369
+ copy,
1370
+ depth: 0,
1371
+ node,
1372
+ view
1373
+ },
1374
+ nodeRefKey(node.ref)
1375
+ ))
1376
+ ),
1377
+ view.hasMore && (view.isQuery || hasSelectedGroup) ? /* @__PURE__ */ jsxs4(
1378
+ Button3,
1379
+ {
1380
+ className: "mt-1 w-full",
1381
+ disabled: view.isLoadingMore,
1382
+ size: "sm",
1383
+ type: "button",
1384
+ variant: "ghost",
1385
+ onClick: view.loadMore,
1386
+ children: [
1387
+ view.isLoadingMore ? /* @__PURE__ */ jsx4(Spinner2, { className: "text-current", size: 14 }) : null,
1388
+ copy.t("referencePicker.loadMore")
1389
+ ]
1390
+ }
1391
+ ) : null
1392
+ ]
1393
+ }
1394
+ )
1214
1395
  }
1215
- ) })
1396
+ )
1216
1397
  ] })
1217
1398
  }
1218
1399
  ),
@@ -1221,6 +1402,7 @@ function ReferenceSourcePicker({
1221
1402
  {
1222
1403
  disableDoubleClick: true,
1223
1404
  withHandle: true,
1405
+ className: "after:bg-[var(--line-1)]",
1224
1406
  onDoubleClick: fitMiddle
1225
1407
  }
1226
1408
  ),
@@ -1228,17 +1410,17 @@ function ReferenceSourcePicker({
1228
1410
  ResizablePanel,
1229
1411
  {
1230
1412
  id: "preview",
1231
- className: "min-h-0 border-l border-[var(--line-1)]",
1413
+ className: "min-h-0 min-w-0",
1232
1414
  defaultSize: 30,
1233
1415
  minSize: "200px",
1234
1416
  children: /* @__PURE__ */ jsx4(
1235
1417
  PreviewInfoPane,
1236
1418
  {
1419
+ copy,
1237
1420
  node: view.focusedNode,
1421
+ previewState: view.previewState,
1238
1422
  sourceLabel: view.activeTabLabel,
1239
- hierarchy: view.breadcrumb,
1240
- onReference: view.toggleSelection,
1241
- referenced: view.focusedNode ? view.isSelected(view.focusedNode) : false
1423
+ hierarchy: resolveHierarchyPath(view)
1242
1424
  }
1243
1425
  )
1244
1426
  }
@@ -1269,81 +1451,125 @@ function ReferenceSourcePicker({
1269
1451
  if (typeof document === "undefined") {
1270
1452
  return dialog;
1271
1453
  }
1272
- return createPortal2(dialog, document.body);
1454
+ return createPortal(dialog, document.body);
1455
+ }
1456
+ function GroupFallbackIcon({
1457
+ icon,
1458
+ className
1459
+ }) {
1460
+ if (icon === "issue") {
1461
+ return /* @__PURE__ */ jsx4(IssueIcon, { className });
1462
+ }
1463
+ return /* @__PURE__ */ jsx4(FolderFilledIcon3, { className });
1273
1464
  }
1274
1465
  function SourceSidebar({
1466
+ copy,
1275
1467
  view,
1276
1468
  contentRef
1277
1469
  }) {
1278
- return /* @__PURE__ */ jsx4(ScrollArea2, { className: "h-full min-h-0 w-full", children: /* @__PURE__ */ jsxs4("div", { ref: contentRef, className: "flex flex-col gap-0.5 p-2", children: [
1279
- /* @__PURE__ */ jsx4("p", { className: "px-2 py-1 text-[11px] font-semibold text-[var(--text-tertiary)]", children: L.sourceColumn }),
1280
- view.tabs.map((tab) => {
1281
- const active = tab.sourceId === view.activeSourceId;
1282
- const expanded = view.isSourceExpanded(tab.sourceId);
1283
- const groups = view.sidebarGroupsBySource[tab.sourceId] ?? [];
1284
- return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-0.5", children: [
1285
- /* @__PURE__ */ jsxs4(
1470
+ const selectedGroupRef = useRef2(null);
1471
+ useEffect3(() => {
1472
+ selectedGroupRef.current?.scrollIntoView({ block: "nearest" });
1473
+ }, [view.selectedGroupKey]);
1474
+ const [shownBySource, setShownBySource] = useState3(
1475
+ {}
1476
+ );
1477
+ const loadMoreGroups = (sourceId) => {
1478
+ const groups = view.sidebarGroupsBySource[sourceId] ?? [];
1479
+ const limit = shownBySource[sourceId] ?? SIDEBAR_GROUP_PAGE_SIZE;
1480
+ const selectedIndex = groups.findIndex(
1481
+ (group) => nodeRefKey(group.ref) === view.selectedGroupKey
1482
+ );
1483
+ const visibleCount = selectedIndex >= limit ? selectedIndex + 1 : limit;
1484
+ const next = visibleCount + SIDEBAR_GROUP_PAGE_SIZE;
1485
+ setShownBySource((prev) => ({ ...prev, [sourceId]: next }));
1486
+ if (next > groups.length && (view.sidebarHasMoreBySource[sourceId] ?? false)) {
1487
+ view.loadMoreSidebarGroups(sourceId);
1488
+ }
1489
+ };
1490
+ return /* @__PURE__ */ jsx4(ScrollArea2, { className: "h-full min-h-0 w-full", children: /* @__PURE__ */ jsx4("div", { ref: contentRef, className: "flex flex-col gap-1 p-2", children: view.tabs.map((tab) => {
1491
+ const groups = view.sidebarGroupsBySource[tab.sourceId] ?? [];
1492
+ const limit = shownBySource[tab.sourceId] ?? SIDEBAR_GROUP_PAGE_SIZE;
1493
+ const selectedIndex = groups.findIndex(
1494
+ (group) => nodeRefKey(group.ref) === view.selectedGroupKey
1495
+ );
1496
+ const effectiveLimit = selectedIndex >= limit ? selectedIndex + 1 : limit;
1497
+ const visibleGroups = groups.slice(0, effectiveLimit);
1498
+ const loadingMore = view.sidebarLoadingMoreBySource[tab.sourceId] ?? false;
1499
+ const hasMore = groups.length > effectiveLimit || (view.sidebarHasMoreBySource[tab.sourceId] ?? false);
1500
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-0.5", children: [
1501
+ /* @__PURE__ */ jsx4(
1502
+ "p",
1503
+ {
1504
+ className: "px-2 pt-1.5 pb-0.5 text-[11px] font-semibold text-[var(--text-tertiary)]",
1505
+ "data-autofit-label": true,
1506
+ children: tab.label
1507
+ }
1508
+ ),
1509
+ groups.length === 0 ? view.isLoadingTabs ? /* @__PURE__ */ jsx4("p", { className: "px-2 py-1 text-[12px] text-[var(--text-tertiary)]", children: "\u2026" }) : null : visibleGroups.map((group) => {
1510
+ const key = nodeRefKey(group.ref);
1511
+ const selected = key === view.selectedGroupKey;
1512
+ return /* @__PURE__ */ jsxs4(
1286
1513
  "button",
1287
1514
  {
1288
- "aria-expanded": expanded,
1515
+ ref: selected ? selectedGroupRef : void 0,
1516
+ "aria-current": selected ? "true" : void 0,
1289
1517
  className: cn3(
1290
- "flex items-center gap-1.5 rounded-[6px] px-2 py-1.5 text-left text-[13px] font-semibold transition-colors hover:bg-transparency-block",
1291
- active ? "text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
1518
+ "flex items-center gap-2 rounded-[6px] px-2 py-1.5 text-left text-[13px] transition-colors",
1519
+ selected ? "bg-primary/10 font-medium text-[var(--text-primary)] hover:bg-primary/15" : "text-[var(--text-secondary)] hover:bg-[var(--transparency-hover)] hover:text-[var(--text-primary)]"
1292
1520
  ),
1293
1521
  type: "button",
1294
- onClick: () => view.toggleSourceExpanded(tab.sourceId),
1522
+ onClick: () => view.selectGroup(group),
1295
1523
  children: [
1524
+ group.iconUrl ? /* @__PURE__ */ jsx4(
1525
+ "img",
1526
+ {
1527
+ alt: "",
1528
+ className: "size-4 shrink-0 rounded-[3px] object-cover",
1529
+ src: group.iconUrl
1530
+ }
1531
+ ) : /* @__PURE__ */ jsx4(
1532
+ GroupFallbackIcon,
1533
+ {
1534
+ className: "size-4 shrink-0 text-[var(--rich-text-folder)]",
1535
+ icon: tab.icon
1536
+ }
1537
+ ),
1296
1538
  /* @__PURE__ */ jsx4(
1297
- ArrowRightIcon2,
1539
+ "span",
1298
1540
  {
1299
- className: cn3(
1300
- "size-3 shrink-0 text-[var(--text-tertiary)] transition-transform",
1301
- expanded && "rotate-90"
1302
- )
1541
+ className: "min-w-0 flex-1 truncate",
1542
+ "data-autofit-label": true,
1543
+ children: group.displayName
1303
1544
  }
1304
1545
  ),
1305
- /* @__PURE__ */ jsx4("span", { className: "truncate", "data-autofit-label": true, children: tab.label })
1546
+ group.childCount != null ? /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[11px] text-[var(--text-tertiary)]", children: group.childCount }) : null
1306
1547
  ]
1307
- }
1308
- ),
1309
- expanded ? /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-0.5", children: groups.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "px-2 py-1.5 pl-7 text-[12px] text-[var(--text-tertiary)]", children: view.isLoadingTabs ? "\u2026" : "" }) : groups.map((group) => {
1310
- const key = nodeRefKey(group.ref);
1311
- const selected = key === view.selectedGroupKey;
1312
- return /* @__PURE__ */ jsxs4(
1313
- "button",
1314
- {
1315
- className: cn3(
1316
- "flex items-center gap-2 rounded-[6px] py-1.5 pr-2 pl-7 text-left text-[13px] transition-colors hover:bg-transparency-block",
1317
- selected ? "bg-transparency-block text-[var(--text-primary)]" : "text-[var(--text-secondary)]"
1318
- ),
1319
- type: "button",
1320
- onClick: () => view.selectGroup(group),
1321
- children: [
1322
- group.iconUrl ? /* @__PURE__ */ jsx4(
1323
- "img",
1324
- {
1325
- alt: "",
1326
- className: "size-4 shrink-0 rounded-[3px] object-cover",
1327
- src: group.iconUrl
1328
- }
1329
- ) : /* @__PURE__ */ jsx4(FolderFilledIcon3, { className: "size-4 shrink-0 text-[var(--rich-text-folder)]" }),
1330
- /* @__PURE__ */ jsx4(
1331
- "span",
1332
- {
1333
- className: "min-w-0 flex-1 truncate",
1334
- "data-autofit-label": true,
1335
- children: group.displayName
1336
- }
1337
- ),
1338
- group.childCount != null ? /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[11px] text-[var(--text-tertiary)]", children: group.childCount }) : null
1339
- ]
1340
- },
1341
- key
1342
- );
1343
- }) }) : null
1344
- ] }, tab.sourceId);
1345
- })
1346
- ] }) });
1548
+ },
1549
+ key
1550
+ );
1551
+ }),
1552
+ hasMore ? /* @__PURE__ */ jsxs4(
1553
+ "button",
1554
+ {
1555
+ className: "flex items-center gap-1.5 rounded-[6px] px-2 py-1.5 text-left text-[12px] text-[var(--text-secondary)] transition-colors hover:bg-[var(--transparency-hover)] hover:text-[var(--text-primary)] disabled:opacity-60",
1556
+ disabled: loadingMore,
1557
+ type: "button",
1558
+ onClick: () => loadMoreGroups(tab.sourceId),
1559
+ children: [
1560
+ loadingMore ? /* @__PURE__ */ jsx4(
1561
+ Spinner2,
1562
+ {
1563
+ className: "text-[var(--text-secondary)]",
1564
+ size: 12
1565
+ }
1566
+ ) : null,
1567
+ /* @__PURE__ */ jsx4("span", { children: copy.t("referencePicker.loadMoreGroups") })
1568
+ ]
1569
+ }
1570
+ ) : null
1571
+ ] }, tab.sourceId);
1572
+ }) }) });
1347
1573
  }
1348
1574
  function SearchResultRow({
1349
1575
  node,
@@ -1357,34 +1583,28 @@ function SearchResultRow({
1357
1583
  "div",
1358
1584
  {
1359
1585
  className: cn3(
1360
- "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-[6px] border py-2.5 pr-1 pl-3 transition-colors",
1586
+ "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",
1361
1587
  focused || selected ? "border-border bg-transparency-block" : "border-transparent bg-transparent hover:border-border/70 hover:bg-transparency-block"
1362
1588
  ),
1589
+ onClick: () => onFocus(node),
1363
1590
  children: [
1364
- /* @__PURE__ */ jsxs4(
1365
- "button",
1366
- {
1367
- className: "flex min-w-0 items-center gap-3 text-left",
1368
- type: "button",
1369
- onClick: () => onFocus(node),
1370
- children: [
1371
- /* @__PURE__ */ jsx4("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-[var(--transparency-block)] text-[var(--text-tertiary)]", children: isFolder ? /* @__PURE__ */ jsx4(FolderFilledIcon3, { className: "size-4 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx4(FileIcon3, { className: "size-4 text-[var(--text-tertiary)]" }) }),
1372
- /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1373
- /* @__PURE__ */ jsx4("span", { className: "block truncate text-[13px] font-medium text-[var(--text-primary)]", children: node.displayName }),
1374
- /* @__PURE__ */ jsx4("span", { className: "block truncate text-[11px] text-[var(--text-secondary)]", children: node.ref.nodeId })
1375
- ] })
1376
- ]
1377
- }
1378
- ),
1591
+ /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 items-center gap-3 text-left", children: [
1592
+ /* @__PURE__ */ jsx4("span", { className: "grid size-9 shrink-0 place-items-center rounded-lg bg-[var(--transparency-block)] text-[var(--text-tertiary)]", children: isFolder ? /* @__PURE__ */ jsx4(FolderFilledIcon3, { className: "size-4 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx4(FileIcon3, { className: "size-4 text-[var(--text-tertiary)]" }) }),
1593
+ /* @__PURE__ */ jsxs4("span", { className: "min-w-0", children: [
1594
+ /* @__PURE__ */ jsx4("span", { className: "block truncate text-[13px] font-medium text-[var(--text-primary)]", children: node.displayName }),
1595
+ /* @__PURE__ */ jsx4("span", { className: "block truncate text-[11px] text-[var(--text-secondary)]", children: node.contextLabel ?? node.ref.nodeId })
1596
+ ] })
1597
+ ] }),
1379
1598
  /* @__PURE__ */ jsx4(
1380
- Button4,
1599
+ Button3,
1381
1600
  {
1382
1601
  "aria-label": node.displayName,
1383
1602
  "aria-pressed": selected,
1384
1603
  size: "icon-sm",
1385
1604
  type: "button",
1386
1605
  variant: "ghost",
1387
- onClick: () => {
1606
+ onClick: (event) => {
1607
+ event.stopPropagation();
1388
1608
  onFocus(node);
1389
1609
  onToggle(node);
1390
1610
  },
@@ -1395,35 +1615,111 @@ function SearchResultRow({
1395
1615
  }
1396
1616
  );
1397
1617
  }
1618
+ function toPreviewSurfaceState(node, previewState, copy) {
1619
+ if (!("node" in previewState) || nodeRefKey(previewState.node.ref) !== nodeRefKey(node.ref)) {
1620
+ return node.kind === "folder" ? { entry: node, status: "directory" } : { entry: node, status: "loading" };
1621
+ }
1622
+ switch (previewState.status) {
1623
+ case "directory":
1624
+ return { entry: node, status: "directory" };
1625
+ case "loading":
1626
+ return { entry: node, status: "loading" };
1627
+ case "image":
1628
+ return {
1629
+ entry: node,
1630
+ objectUrl: previewState.objectUrl,
1631
+ status: "image"
1632
+ };
1633
+ case "text":
1634
+ return { content: previewState.content, entry: node, status: "text" };
1635
+ case "readonly":
1636
+ return {
1637
+ entry: node,
1638
+ message: previewState.reason === "binary" ? copy.t("referencePicker.previewBinary") : previewState.reason === "file_too_large" || previewState.reason === "text_too_large" ? copy.t("referencePicker.previewTooLarge") : copy.t("referencePicker.previewUnsupported"),
1639
+ status: "readonly"
1640
+ };
1641
+ case "error":
1642
+ return {
1643
+ entry: node,
1644
+ message: copy.t("referencePicker.previewError"),
1645
+ status: "error"
1646
+ };
1647
+ case "unsupported":
1648
+ return {
1649
+ entry: node,
1650
+ message: copy.t("referencePicker.previewUnsupported"),
1651
+ status: "unsupported"
1652
+ };
1653
+ }
1654
+ }
1655
+ function sourceBadgeClassName(sourceId) {
1656
+ return SOURCE_BADGE_CLASSES[sourceId];
1657
+ }
1658
+ var SOURCE_BADGE_CLASSES = {
1659
+ "workspace-file": "bg-[color-mix(in_srgb,var(--rich-text-mention-file)_12%,transparent)] text-[var(--rich-text-mention-file)]",
1660
+ "app-artifact": "bg-[color-mix(in_srgb,var(--rich-text-mention-app)_12%,transparent)] text-[var(--rich-text-mention-app)]",
1661
+ "issue-file": "bg-[color-mix(in_srgb,var(--rich-text-mention-issue)_12%,transparent)] text-[var(--rich-text-mention-issue)]"
1662
+ };
1398
1663
  function PreviewInfoPane({
1664
+ copy,
1399
1665
  node,
1666
+ previewState,
1400
1667
  sourceLabel,
1401
- hierarchy,
1402
- referenced,
1403
- onReference
1668
+ hierarchy
1404
1669
  }) {
1405
1670
  return /* @__PURE__ */ jsx4("aside", { className: "flex h-full min-h-0 w-full flex-col bg-[var(--background-fronted)]", children: node ? /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-1 flex-col gap-4 p-4", children: [
1406
- /* @__PURE__ */ jsx4("div", { className: "grid aspect-[3/2] w-full place-items-center rounded-[8px] border border-[var(--line-2,var(--border-2))] bg-[var(--transparency-block)]", children: /* @__PURE__ */ jsx4(FileIcon3, { className: "size-9 text-[var(--text-tertiary)]" }) }),
1671
+ /* @__PURE__ */ jsx4(
1672
+ WorkspaceFilePreviewSurface,
1673
+ {
1674
+ directoryMessage: copy.t("referencePicker.previewFolder"),
1675
+ emptyMessage: copy.t("referencePicker.emptyPreview"),
1676
+ frameClassName: "flex aspect-[3/2] w-full flex-col items-center justify-center overflow-hidden rounded-[8px] border border-[var(--line-2,var(--border-2))] bg-[var(--transparency-block)] p-0 text-center",
1677
+ imageAlt: (entry) => entry.displayName,
1678
+ imageFrameClassName: "p-3",
1679
+ loadingIndicator: /* @__PURE__ */ jsx4(Spinner2, { size: 16 }),
1680
+ loadingMessage: copy.t("referencePicker.previewLoading"),
1681
+ messageClassName: "mx-auto max-w-[24ch] text-[13px] leading-5 text-[var(--text-secondary)] [overflow-wrap:anywhere]",
1682
+ renderIcon: (entry) => entry.kind === "folder" ? /* @__PURE__ */ jsx4(FolderFilledIcon3, { className: "size-9 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx4(FileIcon3, { className: "size-9 text-[var(--text-tertiary)]" }),
1683
+ state: toPreviewSurfaceState(node, previewState, copy),
1684
+ textClassName: "h-full w-full overflow-auto p-3 text-left text-[11px] leading-5 whitespace-pre-wrap break-words text-[var(--text-primary)]",
1685
+ textFrameClassName: "items-stretch justify-stretch"
1686
+ }
1687
+ ),
1407
1688
  /* @__PURE__ */ jsx4("p", { className: "truncate text-[15px] font-semibold", children: node.displayName }),
1408
1689
  /* @__PURE__ */ jsxs4("dl", { className: "space-y-2 text-[13px]", children: [
1409
- /* @__PURE__ */ jsx4(InfoRow, { label: L.previewSource, children: /* @__PURE__ */ jsx4(Badge2, { variant: "secondary", children: sourceLabel }) }),
1410
- node.mtimeMs != null ? /* @__PURE__ */ jsx4(InfoRow, { label: L.previewModified, children: formatDateTime(node.mtimeMs) }) : null,
1411
- node.sizeBytes != null ? /* @__PURE__ */ jsx4(InfoRow, { label: L.previewSize, children: formatBytes(node.sizeBytes) }) : null
1690
+ /* @__PURE__ */ jsx4(InfoRow, { label: copy.t("referencePicker.previewSource"), children: /* @__PURE__ */ jsx4(
1691
+ Badge2,
1692
+ {
1693
+ variant: "secondary",
1694
+ className: sourceBadgeClassName(node.ref.sourceId),
1695
+ children: sourceLabel
1696
+ }
1697
+ ) }),
1698
+ node.mtimeMs != null ? /* @__PURE__ */ jsx4(InfoRow, { label: copy.t("referencePicker.previewModified"), children: formatDateTime(node.mtimeMs) }) : null,
1699
+ node.sizeBytes != null ? /* @__PURE__ */ jsx4(InfoRow, { label: copy.t("referencePicker.previewSize"), children: formatBytes(node.sizeBytes) }) : null
1412
1700
  ] }),
1413
1701
  hierarchy.length > 0 ? /* @__PURE__ */ jsxs4("div", { className: "space-y-1.5", children: [
1414
- /* @__PURE__ */ jsx4("p", { className: "text-[11px] font-semibold text-[var(--text-tertiary)]", children: L.previewHierarchy }),
1415
- /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1", children: hierarchy.map((crumb) => /* @__PURE__ */ jsx4(Badge2, { variant: "secondary", children: crumb.displayName }, nodeRefKey(crumb.ref))) })
1416
- ] }) : null,
1417
- /* @__PURE__ */ jsx4("div", { className: "mt-auto flex justify-end", children: /* @__PURE__ */ jsx4(
1418
- Button4,
1419
- {
1420
- type: "button",
1421
- variant: referenced ? "secondary" : void 0,
1422
- onClick: () => onReference(node),
1423
- children: L.reference
1424
- }
1425
- ) })
1426
- ] }) : /* @__PURE__ */ jsx4(Feedback, { children: L.emptyPreview }) });
1702
+ /* @__PURE__ */ jsx4("p", { className: "text-[11px] font-semibold text-[var(--text-tertiary)]", children: copy.t("referencePicker.previewHierarchy") }),
1703
+ /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap items-center gap-y-1 text-[12px] leading-5", children: hierarchy.map((crumb, index) => /* @__PURE__ */ jsxs4(
1704
+ "span",
1705
+ {
1706
+ className: "flex min-w-0 items-center",
1707
+ children: [
1708
+ index > 0 ? /* @__PURE__ */ jsx4("span", { className: "mx-1 shrink-0 text-[var(--text-tertiary)]", children: "/" }) : null,
1709
+ /* @__PURE__ */ jsx4(
1710
+ "span",
1711
+ {
1712
+ className: "max-w-[14ch] truncate text-[var(--text-secondary)]",
1713
+ title: crumb.displayName,
1714
+ children: crumb.displayName
1715
+ }
1716
+ )
1717
+ ]
1718
+ },
1719
+ nodeRefKey(crumb.ref)
1720
+ )) })
1721
+ ] }) : null
1722
+ ] }) : /* @__PURE__ */ jsx4(Feedback, { children: copy.t("referencePicker.emptyPreview") }) });
1427
1723
  }
1428
1724
  function InfoRow({
1429
1725
  label,
@@ -1446,17 +1742,25 @@ function Footer({
1446
1742
  }) {
1447
1743
  return /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-3 border-t border-[var(--line-1)] px-4 py-3 sm:px-6", children: [
1448
1744
  /* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 items-center gap-2", children: [
1449
- /* @__PURE__ */ jsx4("span", { className: "text-[13px] text-[var(--text-secondary)]", children: countLabel }),
1450
- selection.slice(0, 2).map((node) => /* @__PURE__ */ jsx4(Badge2, { variant: "secondary", children: /* @__PURE__ */ jsx4("span", { className: "truncate", children: node.displayName }) }, nodeRefKey(node.ref))),
1451
- selection.length > 2 ? /* @__PURE__ */ jsxs4(Badge2, { variant: "secondary", children: [
1745
+ /* @__PURE__ */ jsx4("span", { className: "shrink-0 text-[13px] text-[var(--text-secondary)]", children: countLabel }),
1746
+ selection.slice(0, 2).map((node) => /* @__PURE__ */ jsx4(
1747
+ Badge2,
1748
+ {
1749
+ className: "min-w-0 max-w-[12rem]",
1750
+ variant: "secondary",
1751
+ children: /* @__PURE__ */ jsx4("span", { className: "truncate", children: node.displayName })
1752
+ },
1753
+ nodeRefKey(node.ref)
1754
+ )),
1755
+ selection.length > 2 ? /* @__PURE__ */ jsxs4(Badge2, { className: "shrink-0", variant: "secondary", children: [
1452
1756
  "+",
1453
1757
  selection.length - 2
1454
1758
  ] }) : null
1455
1759
  ] }),
1456
1760
  /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1457
- /* @__PURE__ */ jsx4(Button4, { type: "button", variant: "secondary", onClick: onClose, children: cancelLabel }),
1761
+ /* @__PURE__ */ jsx4(Button3, { type: "button", variant: "secondary", onClick: onClose, children: cancelLabel }),
1458
1762
  /* @__PURE__ */ jsxs4(
1459
- Button4,
1763
+ Button3,
1460
1764
  {
1461
1765
  disabled: disabled || loading,
1462
1766
  type: "button",
@@ -1473,11 +1777,150 @@ function Footer({
1473
1777
  function Feedback({ children }) {
1474
1778
  return /* @__PURE__ */ jsx4("div", { className: "grid min-h-0 flex-1 place-items-center px-4 py-8 text-center text-[13px] text-[var(--text-secondary)]", children });
1475
1779
  }
1780
+ function FilterCategoryFilter({
1781
+ categories,
1782
+ copy,
1783
+ selected,
1784
+ onClear,
1785
+ onToggle
1786
+ }) {
1787
+ const [open, setOpen] = useState3(false);
1788
+ const containerRef = useRef2(null);
1789
+ useEffect3(() => {
1790
+ if (!open) {
1791
+ return;
1792
+ }
1793
+ const onPointerDown = (event) => {
1794
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
1795
+ setOpen(false);
1796
+ }
1797
+ };
1798
+ const onKeyDown = (event) => {
1799
+ if (event.key === "Escape") {
1800
+ setOpen(false);
1801
+ }
1802
+ };
1803
+ document.addEventListener("pointerdown", onPointerDown, true);
1804
+ document.addEventListener("keydown", onKeyDown);
1805
+ return () => {
1806
+ document.removeEventListener("pointerdown", onPointerDown, true);
1807
+ document.removeEventListener("keydown", onKeyDown);
1808
+ };
1809
+ }, [open]);
1810
+ const selectedLabels = categories.filter((category) => selected.has(category.id)).map((category) => copy.t(category.labelKey));
1811
+ const count = selectedLabels.length;
1812
+ const labelText = count > 0 ? selectedLabels.join(copy.t("referencePicker.fileTypeSeparator")) : copy.t("referencePicker.fileTypeAll");
1813
+ return (
1814
+ // shrink-0:不被搜索框挤压;固定宽度让控件不随选中文案变宽。
1815
+ /* @__PURE__ */ jsxs4("div", { ref: containerRef, className: "relative w-[124px] shrink-0", children: [
1816
+ /* @__PURE__ */ jsxs4(
1817
+ Button3,
1818
+ {
1819
+ "aria-expanded": open,
1820
+ "aria-haspopup": "menu",
1821
+ className: "h-8 w-full justify-between gap-1.5 border-0 px-2.5",
1822
+ size: "default",
1823
+ type: "button",
1824
+ variant: "secondary",
1825
+ onClick: () => setOpen((current) => !current),
1826
+ children: [
1827
+ /* @__PURE__ */ jsx4("span", { className: "min-w-0 flex-1 truncate text-left", title: labelText, children: labelText }),
1828
+ count > 1 ? /* @__PURE__ */ jsx4(Badge2, { className: "shrink-0 px-1.5", variant: "secondary", children: count }) : null,
1829
+ count > 0 ? (
1830
+ // 有选中时,右侧箭头位让位给「清除」:role=button 的 span(避免 button 嵌
1831
+ // button 的非法结构),stopPropagation 让点击只清空筛选、不触发触发器的开合。
1832
+ /* @__PURE__ */ jsx4(
1833
+ "span",
1834
+ {
1835
+ "aria-label": copy.t("referencePicker.clearFilter"),
1836
+ className: "grid size-4 shrink-0 cursor-pointer place-items-center rounded-full text-[var(--text-tertiary)] transition-colors hover:bg-[var(--transparency-hover)] hover:text-[var(--text-primary)]",
1837
+ role: "button",
1838
+ tabIndex: 0,
1839
+ onClick: (event) => {
1840
+ event.stopPropagation();
1841
+ onClear();
1842
+ },
1843
+ onKeyDown: (event) => {
1844
+ if (event.key === "Enter" || event.key === " ") {
1845
+ event.preventDefault();
1846
+ event.stopPropagation();
1847
+ onClear();
1848
+ }
1849
+ },
1850
+ children: /* @__PURE__ */ jsx4(CloseIcon, { size: 12 })
1851
+ }
1852
+ )
1853
+ ) : /* @__PURE__ */ jsx4(ChevronDownIcon, { className: "size-3.5 shrink-0 text-[var(--text-tertiary)]" })
1854
+ ]
1855
+ }
1856
+ ),
1857
+ open ? /* @__PURE__ */ jsx4(
1858
+ "div",
1859
+ {
1860
+ className: "absolute top-[calc(100%+4px)] right-0 min-w-40 overflow-hidden rounded-[8px] border border-[var(--line-1)] bg-[var(--background-fronted)] p-1 shadow-panel",
1861
+ role: "menu",
1862
+ style: { zIndex: "var(--z-panel-popover)" },
1863
+ children: categories.map((category) => {
1864
+ const checked = selected.has(category.id);
1865
+ return /* @__PURE__ */ jsxs4(
1866
+ "button",
1867
+ {
1868
+ "aria-checked": checked,
1869
+ className: "flex w-full items-center gap-2 rounded-[6px] px-2 py-1.5 text-left text-[13px] text-[var(--text-primary)] transition-colors hover:bg-[var(--transparency-hover)]",
1870
+ role: "menuitemcheckbox",
1871
+ type: "button",
1872
+ onClick: () => onToggle(category.id),
1873
+ children: [
1874
+ /* @__PURE__ */ jsx4("span", { className: "grid size-4 shrink-0 place-items-center", children: checked ? /* @__PURE__ */ jsx4(CheckIcon2, { className: "size-3.5 text-[var(--tutti-purple)]" }) : null }),
1875
+ /* @__PURE__ */ jsx4("span", { className: "flex-1", children: copy.t(category.labelKey) })
1876
+ ]
1877
+ },
1878
+ category.id
1879
+ );
1880
+ })
1881
+ }
1882
+ ) : null
1883
+ ] })
1884
+ );
1885
+ }
1476
1886
  var TREE_INDENT = 28;
1477
1887
  var TREE_COLLAPSE_DURATION_MS = 200;
1478
1888
  function isFocused(focused, node) {
1479
1889
  return focused ? nodeRefKey(focused.ref) === nodeRefKey(node.ref) : false;
1480
1890
  }
1891
+ function resolveHierarchyPath(view) {
1892
+ const focused = view.focusedNode;
1893
+ if (!focused) {
1894
+ return view.breadcrumb;
1895
+ }
1896
+ const trail = findFocusedTrail(
1897
+ view.currentEntries,
1898
+ view.childrenByKey,
1899
+ nodeRefKey(focused.ref),
1900
+ []
1901
+ );
1902
+ return trail ? [...view.breadcrumb, ...trail] : view.breadcrumb;
1903
+ }
1904
+ function findFocusedTrail(roots, childrenByKey, targetKey, trail) {
1905
+ for (const node of roots) {
1906
+ if (nodeRefKey(node.ref) === targetKey) {
1907
+ return [...trail];
1908
+ }
1909
+ if (node.kind === "folder") {
1910
+ const children = childrenByKey[nodeRefKey(node.ref)]?.entries ?? [];
1911
+ if (children.length > 0) {
1912
+ const found = findFocusedTrail(children, childrenByKey, targetKey, [
1913
+ ...trail,
1914
+ node
1915
+ ]);
1916
+ if (found) {
1917
+ return found;
1918
+ }
1919
+ }
1920
+ }
1921
+ }
1922
+ return null;
1923
+ }
1481
1924
  function TreeNodeRow({
1482
1925
  node,
1483
1926
  depth,
@@ -1535,10 +1978,16 @@ function TreeNodeRow({
1535
1978
  "div",
1536
1979
  {
1537
1980
  className: cn3(
1538
- "flex items-center gap-2 rounded-[6px] py-1.5 pr-1 transition-colors",
1981
+ "flex cursor-pointer items-center gap-2 rounded-[6px] py-1.5 pr-1 transition-colors",
1539
1982
  focused || selected ? "bg-transparency-block" : "hover:bg-transparency-block"
1540
1983
  ),
1541
1984
  style: { paddingLeft: `${depth * TREE_INDENT + 8}px` },
1985
+ onClick: () => {
1986
+ view.setFocusedNode(node);
1987
+ if (isFolder) {
1988
+ view.toggleNode(node);
1989
+ }
1990
+ },
1542
1991
  children: [
1543
1992
  isFolder ? /* @__PURE__ */ jsx4(
1544
1993
  "button",
@@ -1546,7 +1995,11 @@ function TreeNodeRow({
1546
1995
  "aria-label": node.displayName,
1547
1996
  className: "grid size-5 shrink-0 place-items-center rounded-sm text-[var(--text-secondary)] hover:bg-[var(--transparency-hover)]",
1548
1997
  type: "button",
1549
- onClick: () => view.toggleNode(node),
1998
+ onClick: (event) => {
1999
+ event.stopPropagation();
2000
+ view.setFocusedNode(node);
2001
+ view.toggleNode(node);
2002
+ },
1550
2003
  children: /* @__PURE__ */ jsx4(
1551
2004
  ArrowRightIcon2,
1552
2005
  {
@@ -1558,32 +2011,17 @@ function TreeNodeRow({
1558
2011
  )
1559
2012
  }
1560
2013
  ) : null,
1561
- /* @__PURE__ */ jsxs4(
1562
- "button",
2014
+ isFolder ? /* @__PURE__ */ jsx4(FolderFilledIcon3, { className: "size-4 shrink-0 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx4(FileIcon3, { className: "size-4 shrink-0 text-[var(--text-tertiary)]" }),
2015
+ /* @__PURE__ */ jsx4(
2016
+ "span",
1563
2017
  {
1564
- className: "flex min-w-0 flex-1 items-center gap-2 text-left",
1565
- type: "button",
1566
- onClick: () => {
1567
- view.setFocusedNode(node);
1568
- if (isFolder) {
1569
- view.toggleNode(node);
1570
- }
1571
- },
1572
- children: [
1573
- isFolder ? /* @__PURE__ */ jsx4(FolderFilledIcon3, { className: "size-4 shrink-0 text-[var(--rich-text-folder)]" }) : /* @__PURE__ */ jsx4(FileIcon3, { className: "size-4 shrink-0 text-[var(--text-tertiary)]" }),
1574
- /* @__PURE__ */ jsx4(
1575
- "span",
1576
- {
1577
- className: "truncate text-[13px] text-[var(--text-primary)]",
1578
- "data-autofit-label": true,
1579
- children: node.displayName
1580
- }
1581
- )
1582
- ]
2018
+ className: "min-w-0 flex-1 truncate text-[13px] text-[var(--text-primary)]",
2019
+ "data-autofit-label": true,
2020
+ children: node.displayName
1583
2021
  }
1584
2022
  ),
1585
2023
  /* @__PURE__ */ jsx4(
1586
- Button4,
2024
+ Button3,
1587
2025
  {
1588
2026
  "aria-label": node.displayName,
1589
2027
  "aria-pressed": selected,
@@ -1591,7 +2029,8 @@ function TreeNodeRow({
1591
2029
  size: "icon-sm",
1592
2030
  type: "button",
1593
2031
  variant: "ghost",
1594
- onClick: () => {
2032
+ onClick: (event) => {
2033
+ event.stopPropagation();
1595
2034
  view.setFocusedNode(node);
1596
2035
  view.toggleSelection(node);
1597
2036
  },