@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.
@@ -4,7 +4,7 @@ import {
4
4
  nodeRefKey,
5
5
  sortReferenceNodes,
6
6
  uniqueWorkspaceFileReferences
7
- } from "./chunk-7C3RZYQE.js";
7
+ } from "./chunk-HRC2MZ5H.js";
8
8
 
9
9
  // src/react/internal/reference/WorkspaceFileReferencePickerState.ts
10
10
  var workspaceFileReferenceDefaultExpandedDepth = 4;
@@ -162,7 +162,15 @@ var defaultDirectoryPath = "/";
162
162
  var defaultSearchDebounceMs = 180;
163
163
  function createWorkspaceFileReferencePickerController(input) {
164
164
  const searchDebounceMs = input.searchDebounceMs ?? defaultSearchDebounceMs;
165
- let browseSequence = 0;
165
+ let nextBrowseSeq = 0;
166
+ const latestBrowseSeqByKey = /* @__PURE__ */ new Map();
167
+ const stampBrowse = (seqKey) => {
168
+ const sequence = ++nextBrowseSeq;
169
+ latestBrowseSeqByKey.set(seqKey, sequence);
170
+ return sequence;
171
+ };
172
+ const isBrowseStale = (seqKey, sequence) => !retained || latestBrowseSeqByKey.get(seqKey) !== sequence;
173
+ const REVEAL_SEQ_KEY = "\0reveal";
166
174
  let previewObjectUrl = null;
167
175
  let previewSequence = 0;
168
176
  let retained = false;
@@ -223,7 +231,7 @@ function createWorkspaceFileReferencePickerController(input) {
223
231
  searchAbortController = null;
224
232
  };
225
233
  const cancelCurrentBrowse = () => {
226
- browseSequence += 1;
234
+ latestBrowseSeqByKey.clear();
227
235
  };
228
236
  const cancelCurrentPreview = () => {
229
237
  previewSequence += 1;
@@ -310,7 +318,8 @@ function createWorkspaceFileReferencePickerController(input) {
310
318
  if (normalizedRoot && snapshot.directoryStateByPath[normalizedRoot]?.loaded) {
311
319
  return;
312
320
  }
313
- const sequence = ++browseSequence;
321
+ const seqKey = normalizedRoot ?? "";
322
+ const sequence = stampBrowse(seqKey);
314
323
  setSnapshot({
315
324
  browseError: null,
316
325
  isBrowseLoading: true
@@ -323,7 +332,7 @@ function createWorkspaceFileReferencePickerController(input) {
323
332
  prefetchDepth: 4,
324
333
  workspaceId: input.workspaceId
325
334
  });
326
- if (!retained || sequence !== browseSequence) {
335
+ if (isBrowseStale(seqKey, sequence)) {
327
336
  return;
328
337
  }
329
338
  setSnapshot({
@@ -338,7 +347,7 @@ function createWorkspaceFileReferencePickerController(input) {
338
347
  return;
339
348
  }
340
349
  const listing = await loadDirectoryListing(activeBrowseRootPath);
341
- if (!retained || sequence !== browseSequence || !listing) {
350
+ if (isBrowseStale(seqKey, sequence) || !listing) {
342
351
  return;
343
352
  }
344
353
  setSnapshot((current) => ({
@@ -356,7 +365,7 @@ function createWorkspaceFileReferencePickerController(input) {
356
365
  isBrowseLoading: false
357
366
  }));
358
367
  } catch (error) {
359
- if (!retained || sequence !== browseSequence) {
368
+ if (isBrowseStale(seqKey, sequence)) {
360
369
  return;
361
370
  }
362
371
  setSnapshot({
@@ -373,7 +382,7 @@ function createWorkspaceFileReferencePickerController(input) {
373
382
  if (!retained || snapshot.directoryStateByPath[folderKey]?.loaded || snapshot.directoryStateByPath[folderKey]?.loading) {
374
383
  return;
375
384
  }
376
- const sequence = ++browseSequence;
385
+ const sequence = stampBrowse(folderKey);
377
386
  setSnapshot((current) => ({
378
387
  ...current,
379
388
  directoryStateByPath: {
@@ -388,7 +397,7 @@ function createWorkspaceFileReferencePickerController(input) {
388
397
  }));
389
398
  try {
390
399
  const listing = await loadDirectoryListing(folderKey);
391
- if (!retained || sequence !== browseSequence || !listing) {
400
+ if (isBrowseStale(folderKey, sequence) || !listing) {
392
401
  return;
393
402
  }
394
403
  setSnapshot((current) => ({
@@ -404,7 +413,7 @@ function createWorkspaceFileReferencePickerController(input) {
404
413
  }
405
414
  }));
406
415
  } catch {
407
- if (!retained || sequence !== browseSequence) {
416
+ if (isBrowseStale(folderKey, sequence)) {
408
417
  return;
409
418
  }
410
419
  setSnapshot((current) => ({
@@ -542,7 +551,7 @@ function createWorkspaceFileReferencePickerController(input) {
542
551
  });
543
552
  return null;
544
553
  }
545
- const sequence = ++browseSequence;
554
+ const sequence = stampBrowse(REVEAL_SEQ_KEY);
546
555
  const loadedDirectories = {};
547
556
  const expandedDirectories = {};
548
557
  const directoryState = (path) => loadedDirectories[path] ?? snapshot.directoryStateByPath[path];
@@ -573,7 +582,7 @@ function createWorkspaceFileReferencePickerController(input) {
573
582
  loading: false
574
583
  };
575
584
  }
576
- if (!retained || sequence !== browseSequence) {
585
+ if (isBrowseStale(REVEAL_SEQ_KEY, sequence)) {
577
586
  return null;
578
587
  }
579
588
  setSnapshot((current) => ({
@@ -899,6 +908,8 @@ var ROOT_CHILDREN_KEY = nodeRefKey({
899
908
  nodeId: SOURCE_ROOT_NODE_ID
900
909
  });
901
910
  var defaultSearchDebounceMs2 = 180;
911
+ var SEARCH_PAGE_SIZE = 30;
912
+ var SEARCH_MAX_LIMIT = 200;
902
913
  function emptyTabState(sourceId) {
903
914
  return {
904
915
  sourceId,
@@ -906,8 +917,13 @@ function emptyTabState(sourceId) {
906
917
  childrenByKey: {},
907
918
  mode: "browse",
908
919
  searchQuery: "",
920
+ searchFilters: [],
921
+ searchScopeNodeId: null,
909
922
  searchEntries: [],
910
923
  searchNextCursor: null,
924
+ searchLimit: 0,
925
+ searchHasMore: false,
926
+ isSearchLoadingMore: false,
911
927
  isSearchLoading: false,
912
928
  searchError: null
913
929
  };
@@ -925,8 +941,10 @@ function createReferenceSourcePickerController(input) {
925
941
  const { aggregator, scope } = input;
926
942
  const searchDebounceMs = input.searchDebounceMs ?? defaultSearchDebounceMs2;
927
943
  let retained = false;
944
+ let tabsReady = Promise.resolve();
928
945
  let tabsSequence = 0;
929
- let browseSequence = 0;
946
+ let nextBrowseSeq = 0;
947
+ const latestBrowseSeqByKey = /* @__PURE__ */ new Map();
930
948
  let searchSequence = 0;
931
949
  let searchAbortController = null;
932
950
  let searchTimer = null;
@@ -987,7 +1005,9 @@ function createReferenceSourcePickerController(input) {
987
1005
  if (options.append && !cursor) {
988
1006
  return;
989
1007
  }
990
- const sequence = ++browseSequence;
1008
+ const seqKey = `${sourceId}\0${key}`;
1009
+ const sequence = ++nextBrowseSeq;
1010
+ latestBrowseSeqByKey.set(seqKey, sequence);
991
1011
  setChildrenState(sourceId, key, { loading: true, error: null });
992
1012
  try {
993
1013
  const result = await aggregator.listChildren(
@@ -995,7 +1015,7 @@ function createReferenceSourcePickerController(input) {
995
1015
  node ? node.ref : { sourceId, nodeId: SOURCE_ROOT_NODE_ID },
996
1016
  { cursor }
997
1017
  );
998
- if (!retained || sequence !== browseSequence) {
1018
+ if (!retained || latestBrowseSeqByKey.get(seqKey) !== sequence) {
999
1019
  return;
1000
1020
  }
1001
1021
  const prior = snapshot.bySource[sourceId]?.childrenByKey[key]?.entries ?? [];
@@ -1008,7 +1028,7 @@ function createReferenceSourcePickerController(input) {
1008
1028
  error: null
1009
1029
  });
1010
1030
  } catch (error) {
1011
- if (!retained || sequence !== browseSequence) {
1031
+ if (!retained || latestBrowseSeqByKey.get(seqKey) !== sequence) {
1012
1032
  return;
1013
1033
  }
1014
1034
  setChildrenState(sourceId, key, {
@@ -1073,7 +1093,7 @@ function createReferenceSourcePickerController(input) {
1073
1093
  searchAbortController?.abort();
1074
1094
  searchAbortController = null;
1075
1095
  };
1076
- const runSearch = async (sourceId, query) => {
1096
+ const runSearch = async (sourceId, query, filters, scopeNodeId, limit, loadingMore) => {
1077
1097
  if (!retained) {
1078
1098
  return;
1079
1099
  }
@@ -1083,13 +1103,16 @@ function createReferenceSourcePickerController(input) {
1083
1103
  searchAbortController = abortController;
1084
1104
  updateTab(sourceId, (tab) => ({
1085
1105
  ...tab,
1086
- isSearchLoading: true,
1106
+ ...loadingMore ? { isSearchLoadingMore: true } : { isSearchLoading: true },
1087
1107
  searchError: null
1088
1108
  }));
1089
1109
  try {
1090
1110
  const result = await aggregator.search(scope, sourceId, {
1091
1111
  query,
1092
- signal: abortController.signal
1112
+ limit,
1113
+ signal: abortController.signal,
1114
+ ...filters.length > 0 ? { filters } : {},
1115
+ ...scopeNodeId == null ? {} : { withinNodeId: scopeNodeId }
1093
1116
  });
1094
1117
  if (!retained || sequence !== searchSequence) {
1095
1118
  return;
@@ -1097,8 +1120,12 @@ function createReferenceSourcePickerController(input) {
1097
1120
  updateTab(sourceId, (tab) => ({
1098
1121
  ...tab,
1099
1122
  isSearchLoading: false,
1123
+ isSearchLoadingMore: false,
1100
1124
  searchEntries: sortReferenceNodes(result.entries),
1101
1125
  searchNextCursor: result.nextCursor ?? null,
1126
+ searchLimit: limit,
1127
+ // 本页返回数达到请求上限、且未触全局上限 → 认为可能还有更多(增长式分页启发式)。
1128
+ searchHasMore: result.entries.length >= limit && limit < SEARCH_MAX_LIMIT,
1102
1129
  searchError: null
1103
1130
  }));
1104
1131
  } catch (error) {
@@ -1108,7 +1135,9 @@ function createReferenceSourcePickerController(input) {
1108
1135
  updateTab(sourceId, (tab) => ({
1109
1136
  ...tab,
1110
1137
  isSearchLoading: false,
1111
- searchEntries: [],
1138
+ isSearchLoadingMore: false,
1139
+ // 加载更多失败时保留已有结果,仅新查询失败才清空。
1140
+ ...loadingMore ? {} : { searchEntries: [], searchHasMore: false },
1112
1141
  searchError: normalizeError(error, "reference search failed")
1113
1142
  }));
1114
1143
  } finally {
@@ -1144,18 +1173,32 @@ function createReferenceSourcePickerController(input) {
1144
1173
  await walk(folder);
1145
1174
  return files;
1146
1175
  };
1147
- const scheduleSearch = (sourceId, query) => {
1176
+ const scheduleSearch = (sourceId, query, filters, scopeNodeId) => {
1148
1177
  clearSearchTimer();
1149
- if (!retained || !query) {
1178
+ if (!retained || !query && filters.length === 0) {
1150
1179
  return;
1151
1180
  }
1152
1181
  if (searchDebounceMs <= 0) {
1153
- void runSearch(sourceId, query);
1182
+ void runSearch(
1183
+ sourceId,
1184
+ query,
1185
+ filters,
1186
+ scopeNodeId,
1187
+ SEARCH_PAGE_SIZE,
1188
+ false
1189
+ );
1154
1190
  return;
1155
1191
  }
1156
1192
  searchTimer = setTimeout(() => {
1157
1193
  searchTimer = null;
1158
- void runSearch(sourceId, query);
1194
+ void runSearch(
1195
+ sourceId,
1196
+ query,
1197
+ filters,
1198
+ scopeNodeId,
1199
+ SEARCH_PAGE_SIZE,
1200
+ false
1201
+ );
1159
1202
  }, searchDebounceMs);
1160
1203
  };
1161
1204
  return {
@@ -1170,17 +1213,17 @@ function createReferenceSourcePickerController(input) {
1170
1213
  return;
1171
1214
  }
1172
1215
  retained = true;
1173
- void loadTabs();
1216
+ tabsReady = loadTabs();
1174
1217
  },
1175
1218
  close() {
1176
1219
  retained = false;
1177
1220
  cancelSearch();
1178
- browseSequence += 1;
1221
+ latestBrowseSeqByKey.clear();
1179
1222
  tabsSequence += 1;
1180
1223
  },
1181
1224
  reset() {
1182
1225
  cancelSearch();
1183
- browseSequence += 1;
1226
+ latestBrowseSeqByKey.clear();
1184
1227
  tabsSequence += 1;
1185
1228
  setSnapshot({
1186
1229
  isLoadingTabs: false,
@@ -1192,17 +1235,77 @@ function createReferenceSourcePickerController(input) {
1192
1235
  });
1193
1236
  },
1194
1237
  setActiveSource(sourceId) {
1195
- if (!snapshot.tabs.some((tab2) => tab2.sourceId === sourceId)) {
1238
+ if (!snapshot.tabs.some((tab) => tab.sourceId === sourceId)) {
1196
1239
  return;
1197
1240
  }
1241
+ const prevTab = snapshot.activeSourceId ? snapshot.bySource[snapshot.activeSourceId] : void 0;
1242
+ const carriedQuery = prevTab?.searchQuery ?? "";
1243
+ const carriedFilters = prevTab?.searchFilters ?? [];
1244
+ const trimmed = carriedQuery.trim();
1198
1245
  cancelSearch();
1199
1246
  setSnapshot({ activeSourceId: sourceId });
1200
- const tab = snapshot.bySource[sourceId];
1201
- if (tab?.mode === "search" && tab.searchQuery.trim()) {
1202
- scheduleSearch(sourceId, tab.searchQuery.trim());
1203
- } else {
1247
+ if (trimmed === "" && carriedFilters.length === 0) {
1248
+ updateTab(
1249
+ sourceId,
1250
+ (tab) => tab.mode === "browse" && tab.searchQuery === "" && tab.searchFilters.length === 0 ? tab : {
1251
+ ...tab,
1252
+ mode: "browse",
1253
+ searchQuery: "",
1254
+ searchFilters: [],
1255
+ searchEntries: [],
1256
+ searchHasMore: false,
1257
+ isSearchLoading: false,
1258
+ isSearchLoadingMore: false,
1259
+ searchError: null
1260
+ }
1261
+ );
1204
1262
  ensureRootLoaded(sourceId);
1263
+ return;
1205
1264
  }
1265
+ const nextScopeNodeId = snapshot.bySource[sourceId]?.searchScopeNodeId ?? null;
1266
+ updateTab(sourceId, (tab) => ({
1267
+ ...tab,
1268
+ searchQuery: carriedQuery,
1269
+ searchFilters: carriedFilters,
1270
+ searchScopeNodeId: nextScopeNodeId,
1271
+ mode: "search",
1272
+ // 沿用旧结果直到新结果就绪,避免切源瞬间闪空。
1273
+ isSearchLoading: true,
1274
+ searchError: null
1275
+ }));
1276
+ scheduleSearch(sourceId, trimmed, carriedFilters, nextScopeNodeId);
1277
+ },
1278
+ async locatePath(target) {
1279
+ await tabsReady;
1280
+ if (!retained) {
1281
+ return [];
1282
+ }
1283
+ const refs = await aggregator.locateTarget(
1284
+ scope,
1285
+ target.sourceId,
1286
+ target.params
1287
+ );
1288
+ if (!refs || refs.length === 0) {
1289
+ return [];
1290
+ }
1291
+ const path = [];
1292
+ let parent = {
1293
+ sourceId: target.sourceId,
1294
+ nodeId: SOURCE_ROOT_NODE_ID
1295
+ };
1296
+ for (const ref of refs) {
1297
+ const { entries } = await aggregator.listChildren(scope, parent);
1298
+ const targetKey = nodeRefKey(ref);
1299
+ const node = entries.find(
1300
+ (entry) => nodeRefKey(entry.ref) === targetKey
1301
+ );
1302
+ if (!node) {
1303
+ break;
1304
+ }
1305
+ path.push(node);
1306
+ parent = ref;
1307
+ }
1308
+ return path;
1206
1309
  },
1207
1310
  ensureChildren(node) {
1208
1311
  const sourceId = node ? node.ref.sourceId : snapshot.activeSourceId;
@@ -1245,26 +1348,108 @@ function createReferenceSourcePickerController(input) {
1245
1348
  }
1246
1349
  void loadChildren(sourceId, node, { append: true });
1247
1350
  },
1248
- setSearchQuery(query) {
1351
+ loadMoreSourceRoot(sourceId) {
1352
+ if (!sourceId) {
1353
+ return;
1354
+ }
1355
+ void loadChildren(sourceId, null, { append: true });
1356
+ },
1357
+ setSearchQuery(query, scopeNodeId = null) {
1249
1358
  const sourceId = snapshot.activeSourceId;
1250
1359
  if (!sourceId) {
1251
1360
  return;
1252
1361
  }
1362
+ const filters = snapshot.bySource[sourceId]?.searchFilters ?? [];
1253
1363
  const trimmed = query.trim();
1254
- const nextMode = trimmed ? "search" : "browse";
1364
+ const nextMode = trimmed || filters.length > 0 ? "search" : "browse";
1255
1365
  updateTab(sourceId, (tab) => ({
1256
1366
  ...tab,
1257
1367
  searchQuery: query,
1368
+ searchScopeNodeId: scopeNodeId,
1258
1369
  mode: nextMode,
1259
- ...nextMode === "browse" ? { isSearchLoading: false, searchEntries: [], searchError: null } : {}
1370
+ // 进入搜索:立刻置 loading(搜索是 debounce 的,否则键入到取数之间会先渲染空态,
1371
+ // 造成「空态 → spinner → 结果」闪烁)。保留上次 searchEntries,细化关键词时沿用旧结果而非闪空。
1372
+ ...nextMode === "browse" ? { isSearchLoading: false, searchEntries: [], searchError: null } : { isSearchLoading: true, searchError: null }
1260
1373
  }));
1261
1374
  if (nextMode === "search") {
1262
- scheduleSearch(sourceId, trimmed);
1375
+ scheduleSearch(sourceId, trimmed, filters, scopeNodeId);
1263
1376
  } else {
1264
1377
  cancelSearch();
1265
1378
  ensureRootLoaded(sourceId);
1266
1379
  }
1267
1380
  },
1381
+ setSearchFilters(filters, scopeNodeId = null) {
1382
+ const sourceId = snapshot.activeSourceId;
1383
+ if (!sourceId) {
1384
+ return;
1385
+ }
1386
+ const tab = snapshot.bySource[sourceId];
1387
+ const trimmed = tab?.searchQuery.trim() ?? "";
1388
+ const scopeId = scopeNodeId ?? tab?.searchScopeNodeId ?? null;
1389
+ const nextMode = trimmed || filters.length > 0 ? "search" : "browse";
1390
+ updateTab(sourceId, (current) => ({
1391
+ ...current,
1392
+ searchFilters: filters,
1393
+ searchScopeNodeId: scopeId,
1394
+ mode: nextMode,
1395
+ ...nextMode === "browse" ? { isSearchLoading: false, searchEntries: [], searchError: null } : { isSearchLoading: true, searchError: null }
1396
+ }));
1397
+ if (nextMode === "search") {
1398
+ scheduleSearch(sourceId, trimmed, filters, scopeId);
1399
+ } else {
1400
+ cancelSearch();
1401
+ ensureRootLoaded(sourceId);
1402
+ }
1403
+ },
1404
+ setSearchScope(scopeNodeId) {
1405
+ const sourceId = snapshot.activeSourceId;
1406
+ if (!sourceId) {
1407
+ return;
1408
+ }
1409
+ const tab = snapshot.bySource[sourceId];
1410
+ if (!tab || tab.searchScopeNodeId === scopeNodeId) {
1411
+ return;
1412
+ }
1413
+ updateTab(sourceId, (current) => ({
1414
+ ...current,
1415
+ searchScopeNodeId: scopeNodeId
1416
+ }));
1417
+ const trimmed = tab.searchQuery.trim();
1418
+ const filters = tab.searchFilters;
1419
+ if (tab.mode === "search" && (trimmed || filters.length > 0)) {
1420
+ scheduleSearch(sourceId, trimmed, filters, scopeNodeId);
1421
+ }
1422
+ },
1423
+ loadMoreSearch() {
1424
+ const sourceId = snapshot.activeSourceId;
1425
+ if (!sourceId) {
1426
+ return;
1427
+ }
1428
+ const tab = snapshot.bySource[sourceId];
1429
+ if (!tab || tab.mode !== "search" || !tab.searchHasMore || tab.isSearchLoadingMore) {
1430
+ return;
1431
+ }
1432
+ const trimmed = tab.searchQuery.trim();
1433
+ if (!trimmed && tab.searchFilters.length === 0) {
1434
+ return;
1435
+ }
1436
+ const nextLimit = Math.min(
1437
+ (tab.searchLimit || SEARCH_PAGE_SIZE) + SEARCH_PAGE_SIZE,
1438
+ SEARCH_MAX_LIMIT
1439
+ );
1440
+ if (nextLimit <= tab.searchLimit) {
1441
+ return;
1442
+ }
1443
+ clearSearchTimer();
1444
+ void runSearch(
1445
+ sourceId,
1446
+ trimmed,
1447
+ tab.searchFilters,
1448
+ tab.searchScopeNodeId,
1449
+ nextLimit,
1450
+ true
1451
+ );
1452
+ },
1268
1453
  toggleSelection(node) {
1269
1454
  const key = nodeRefKey(node.ref);
1270
1455
  setSnapshot((current) => {
@@ -1306,6 +1491,38 @@ function createReferenceSourcePickerController(input) {
1306
1491
  }
1307
1492
  }
1308
1493
  return resolved;
1494
+ },
1495
+ async confirmGrouped() {
1496
+ const files = [];
1497
+ const bundles = [];
1498
+ const seenPaths = /* @__PURE__ */ new Set();
1499
+ const pushFile = (ref) => {
1500
+ if (seenPaths.has(ref.path)) {
1501
+ return;
1502
+ }
1503
+ seenPaths.add(ref.path);
1504
+ files.push(ref);
1505
+ };
1506
+ for (const node of snapshot.selection) {
1507
+ const navigable = node.kind === "folder" && (aggregator.getLoadedSource(node.ref.sourceId)?.capabilities.navigable ?? false);
1508
+ if (!navigable) {
1509
+ pushFile(aggregator.resolveSelection(node));
1510
+ continue;
1511
+ }
1512
+ const fileNodes = await collectFolderFiles(node);
1513
+ const bundleSeen = /* @__PURE__ */ new Set();
1514
+ const bundleFiles = [];
1515
+ for (const fileNode of fileNodes) {
1516
+ const ref = aggregator.resolveSelection(fileNode);
1517
+ if (bundleSeen.has(ref.path)) {
1518
+ continue;
1519
+ }
1520
+ bundleSeen.add(ref.path);
1521
+ bundleFiles.push(ref);
1522
+ }
1523
+ bundles.push({ root: node, files: bundleFiles });
1524
+ }
1525
+ return { files, bundles };
1309
1526
  }
1310
1527
  };
1311
1528
  }
@@ -1328,4 +1545,4 @@ export {
1328
1545
  ROOT_CHILDREN_KEY,
1329
1546
  createReferenceSourcePickerController
1330
1547
  };
1331
- //# sourceMappingURL=chunk-CD3YNHYJ.js.map
1548
+ //# sourceMappingURL=chunk-KSK6PILG.js.map