dsh-codegraph-visualizer 0.3.0 → 0.3.1

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.
@@ -2049,7 +2049,7 @@ function resetLoggerListeners() {
2049
2049
 
2050
2050
  //#endregion
2051
2051
  //#region src/client/cache/IndexedDBCache.ts
2052
- const log$45 = scoped("indexeddb-cache");
2052
+ const log$46 = scoped("indexeddb-cache");
2053
2053
  const DB_PREFIX = "codegraph-cache-";
2054
2054
  const CLUSTER_STORE = "cluster-cache";
2055
2055
  const DIAGRAM_STORE = "diagram-cache";
@@ -2100,7 +2100,7 @@ var IndexedDBCache = class {
2100
2100
  }
2101
2101
  getDB() {
2102
2102
  if (!this.dbPromise) this.dbPromise = openDB(this.repoId).catch((err) => {
2103
- log$45.warn("failed to open IndexedDB", {
2103
+ log$46.warn("failed to open IndexedDB", {
2104
2104
  repoId: this.repoId,
2105
2105
  error: err
2106
2106
  });
@@ -2131,7 +2131,7 @@ var IndexedDBCache = class {
2131
2131
  tx.onerror = () => reject(tx.error);
2132
2132
  });
2133
2133
  } catch (e) {
2134
- log$45.warn("IndexedDB invalidateCluster failed", { error: e });
2134
+ log$46.warn("IndexedDB invalidateCluster failed", { error: e });
2135
2135
  }
2136
2136
  }
2137
2137
  async invalidateDiagram(viewMode) {
@@ -2158,7 +2158,7 @@ var IndexedDBCache = class {
2158
2158
  tx.onerror = () => reject(tx.error);
2159
2159
  });
2160
2160
  } catch (e) {
2161
- log$45.warn("IndexedDB invalidateDiagram failed", {
2161
+ log$46.warn("IndexedDB invalidateDiagram failed", {
2162
2162
  viewMode,
2163
2163
  error: e
2164
2164
  });
@@ -2185,7 +2185,7 @@ var IndexedDBCache = class {
2185
2185
  async setMany(storeName, entries) {
2186
2186
  if (entries.length === 0) return;
2187
2187
  if (entries.length > INDEXED_DB.BATCH.MAX_ENTRIES) {
2188
- log$45.warn("setMany batch exceeds limit, splitting", {
2188
+ log$46.warn("setMany batch exceeds limit, splitting", {
2189
2189
  count: entries.length,
2190
2190
  limit: INDEXED_DB.BATCH.MAX_ENTRIES
2191
2191
  });
@@ -2211,7 +2211,7 @@ var IndexedDBCache = class {
2211
2211
  tx.onerror = () => reject(tx.error);
2212
2212
  });
2213
2213
  } catch (e) {
2214
- log$45.warn(`IndexedDB setMany(${storeName}) failed`, {
2214
+ log$46.warn(`IndexedDB setMany(${storeName}) failed`, {
2215
2215
  count: entries.length,
2216
2216
  error: e
2217
2217
  });
@@ -2227,7 +2227,7 @@ var IndexedDBCache = class {
2227
2227
  usageRatio: quota > 0 ? usage / quota : 0
2228
2228
  };
2229
2229
  } catch (e) {
2230
- log$45.warn("storage.estimate() failed", { error: e });
2230
+ log$46.warn("storage.estimate() failed", { error: e });
2231
2231
  return null;
2232
2232
  }
2233
2233
  }
@@ -2239,7 +2239,7 @@ var IndexedDBCache = class {
2239
2239
  async purgeExpired() {
2240
2240
  let totalPurged = 0;
2241
2241
  for (const storeName of ALL_STORES) totalPurged += await this.purgeExpiredStore(storeName);
2242
- if (totalPurged > 0) log$45.info("purged expired entries", { count: totalPurged });
2242
+ if (totalPurged > 0) log$46.info("purged expired entries", { count: totalPurged });
2243
2243
  return totalPurged;
2244
2244
  }
2245
2245
  async purgeExpiredStore(storeName) {
@@ -2271,7 +2271,7 @@ var IndexedDBCache = class {
2271
2271
  });
2272
2272
  return expiredKeys.length;
2273
2273
  } catch (e) {
2274
- log$45.warn(`purgeExpiredStore(${storeName}) failed`, { error: e });
2274
+ log$46.warn(`purgeExpiredStore(${storeName}) failed`, { error: e });
2275
2275
  return 0;
2276
2276
  }
2277
2277
  }
@@ -2294,7 +2294,7 @@ var IndexedDBCache = class {
2294
2294
  }
2295
2295
  return raw;
2296
2296
  } catch (e) {
2297
- log$45.warn(`IndexedDB getFromStore(${storeName}) failed`, {
2297
+ log$46.warn(`IndexedDB getFromStore(${storeName}) failed`, {
2298
2298
  key,
2299
2299
  error: e
2300
2300
  });
@@ -2317,7 +2317,7 @@ var IndexedDBCache = class {
2317
2317
  tx.onerror = () => reject(tx.error);
2318
2318
  });
2319
2319
  } catch (e) {
2320
- log$45.warn(`IndexedDB setToStore(${storeName}) failed`, {
2320
+ log$46.warn(`IndexedDB setToStore(${storeName}) failed`, {
2321
2321
  key,
2322
2322
  error: e
2323
2323
  });
@@ -2333,7 +2333,7 @@ var IndexedDBCache = class {
2333
2333
  tx.onerror = () => reject(tx.error);
2334
2334
  });
2335
2335
  } catch (e) {
2336
- log$45.warn(`IndexedDB deleteFromStore(${storeName}) failed`, {
2336
+ log$46.warn(`IndexedDB deleteFromStore(${storeName}) failed`, {
2337
2337
  key,
2338
2338
  error: e
2339
2339
  });
@@ -2349,7 +2349,7 @@ var IndexedDBCache = class {
2349
2349
 
2350
2350
  //#endregion
2351
2351
  //#region src/client/cache/DiagramCache.ts
2352
- const log$44 = scoped("cache");
2352
+ const log$45 = scoped("cache");
2353
2353
  function computeDataHash(nodes, edges) {
2354
2354
  let nodeHash = 2166136261;
2355
2355
  for (const n of nodes) {
@@ -2441,7 +2441,7 @@ var DiagramCache = class {
2441
2441
  return diagram;
2442
2442
  }
2443
2443
  } catch (e) {
2444
- log$44.warn("IndexedDB diagram getAsync failed", e);
2444
+ log$45.warn("IndexedDB diagram getAsync failed", e);
2445
2445
  }
2446
2446
  return null;
2447
2447
  }
@@ -2453,13 +2453,13 @@ var DiagramCache = class {
2453
2453
  if (firstKey) this.cache.delete(firstKey);
2454
2454
  }
2455
2455
  this.cache.set(key, diagram);
2456
- if (this.idbCache) this.idbCache.setDiagram(key, diagram).catch((e) => log$44.warn("IndexedDB diagram set failed", e));
2456
+ if (this.idbCache) this.idbCache.setDiagram(key, diagram).catch((e) => log$45.warn("IndexedDB diagram set failed", e));
2457
2457
  }
2458
2458
  invalidate(viewMode) {
2459
2459
  if (viewMode) {
2460
2460
  for (const key of this.cache.keys()) if (key.startsWith(`${viewMode}:`)) this.cache.delete(key);
2461
2461
  } else this.cache.clear();
2462
- if (this.idbCache) this.idbCache.invalidateDiagram(viewMode).catch((e) => log$44.warn("IndexedDB diagram invalidate failed", e));
2462
+ if (this.idbCache) this.idbCache.invalidateDiagram(viewMode).catch((e) => log$45.warn("IndexedDB diagram invalidate failed", e));
2463
2463
  }
2464
2464
  size() {
2465
2465
  return this.cache.size;
@@ -2504,7 +2504,7 @@ var ClusterCache = class {
2504
2504
  return result;
2505
2505
  }
2506
2506
  } catch (e) {
2507
- log$44.warn("IndexedDB cluster getAsync failed", e);
2507
+ log$45.warn("IndexedDB cluster getAsync failed", e);
2508
2508
  }
2509
2509
  return null;
2510
2510
  }
@@ -2516,11 +2516,11 @@ var ClusterCache = class {
2516
2516
  if (firstKey) this.cache.delete(firstKey);
2517
2517
  }
2518
2518
  this.cache.set(key, result);
2519
- if (this.idbCache) this.idbCache.setCluster(key, result).catch((e) => log$44.warn("IndexedDB cluster set failed", e));
2519
+ if (this.idbCache) this.idbCache.setCluster(key, result).catch((e) => log$45.warn("IndexedDB cluster set failed", e));
2520
2520
  }
2521
2521
  invalidate() {
2522
2522
  this.cache.clear();
2523
- if (this.idbCache) this.idbCache.invalidateCluster().catch((e) => log$44.warn("IndexedDB cluster invalidate failed", e));
2523
+ if (this.idbCache) this.idbCache.invalidateCluster().catch((e) => log$45.warn("IndexedDB cluster invalidate failed", e));
2524
2524
  }
2525
2525
  size() {
2526
2526
  return this.cache.size;
@@ -4174,6 +4174,25 @@ function buildStylesheet(theme, nodeCount = 0) {
4174
4174
  "border-color": successColor
4175
4175
  }
4176
4176
  },
4177
+ {
4178
+ selector: "node.multi-selected",
4179
+ style: {
4180
+ "border-width": 3,
4181
+ "border-color": accentColor,
4182
+ "background-color": accentColor,
4183
+ "background-opacity": .35,
4184
+ "z-index": 26
4185
+ }
4186
+ },
4187
+ {
4188
+ selector: "node.canvas-focused",
4189
+ style: {
4190
+ "border-width": 2,
4191
+ "border-color": accentColor,
4192
+ "border-style": "dashed",
4193
+ "z-index": 24
4194
+ }
4195
+ },
4177
4196
  {
4178
4197
  selector: ".call-chain",
4179
4198
  style: {
@@ -5502,7 +5521,7 @@ var CallChainAggregator = class {
5502
5521
 
5503
5522
  //#endregion
5504
5523
  //#region src/shared/PerformanceMonitor.ts
5505
- const log$43 = scoped("perf");
5524
+ const log$44 = scoped("perf");
5506
5525
  const MAX_HISTORY = 100;
5507
5526
  const measurements = [];
5508
5527
  const thresholds = new Map();
@@ -5604,8 +5623,8 @@ function recordMeasurement(name$1, durationMs) {
5604
5623
  if (measurements.length > MAX_HISTORY) measurements.shift();
5605
5624
  const threshold = thresholds.get(name$1);
5606
5625
  if (threshold) {
5607
- if (threshold.degradeMs !== void 0 && durationMs >= threshold.degradeMs && threshold.degradationKey) log$43.warn(`${name$1} exceeded degrade threshold: ${durationMs.toFixed(1)}ms >= ${threshold.degradeMs}ms`);
5608
- else if (threshold.warnMs !== void 0 && durationMs >= threshold.warnMs) log$43.info(`${name$1} exceeded warn threshold: ${durationMs.toFixed(1)}ms >= ${threshold.warnMs}ms`);
5626
+ if (threshold.degradeMs !== void 0 && durationMs >= threshold.degradeMs && threshold.degradationKey) log$44.warn(`${name$1} exceeded degrade threshold: ${durationMs.toFixed(1)}ms >= ${threshold.degradeMs}ms`);
5627
+ else if (threshold.warnMs !== void 0 && durationMs >= threshold.warnMs) log$44.info(`${name$1} exceeded warn threshold: ${durationMs.toFixed(1)}ms >= ${threshold.warnMs}ms`);
5609
5628
  }
5610
5629
  }
5611
5630
  function getMeasurements(name$1) {
@@ -5615,7 +5634,7 @@ function getMeasurements(name$1) {
5615
5634
 
5616
5635
  //#endregion
5617
5636
  //#region src/client/store/internals.ts
5618
- const log$42 = scoped("store");
5637
+ const log$43 = scoped("store");
5619
5638
  const diagramCache = new DiagramCache(10);
5620
5639
  const clusterCache = new ClusterCache(20);
5621
5640
  const callChainAggregator = new CallChainAggregator();
@@ -5733,7 +5752,7 @@ function mergeAndRecluster(newNodes, newEdges, set$1, logLabel, logDetail) {
5733
5752
  nodes: result.nodes,
5734
5753
  edges: result.edges
5735
5754
  });
5736
- log$42.info(logLabel, {
5755
+ log$43.info(logLabel, {
5737
5756
  ...logDetail,
5738
5757
  newNodes: filteredNodes.length,
5739
5758
  newEdges: filteredEdges.length
@@ -5905,7 +5924,7 @@ function normalizeEdgeType(type) {
5905
5924
 
5906
5925
  //#endregion
5907
5926
  //#region src/shared/GraphValidator.ts
5908
- const log$41 = scoped("validator");
5927
+ const log$42 = scoped("validator");
5909
5928
  function coerceNode(raw, i$1) {
5910
5929
  if (!raw || typeof raw !== "object") return null;
5911
5930
  const o = raw;
@@ -7165,6 +7184,7 @@ const en = {
7165
7184
  "canvasControls.shortcutZoomIn": "+ Zoom in",
7166
7185
  "canvasControls.shortcutZoomOut": "- Zoom out",
7167
7186
  "canvasControls.shortcutFit": "F Fit to view",
7187
+ "canvasControls.shortcutBoxSelect": "Shift+Drag Box-select nodes",
7168
7188
  "canvasControls.multiSelect": "Multi-select mode",
7169
7189
  "micro.closeView": "Close micro view",
7170
7190
  "micro.functionBar": "Micro view toolbar",
@@ -7328,6 +7348,12 @@ const en = {
7328
7348
  "contextMenu.findPath": "Find Path From Here",
7329
7349
  "contextMenu.showAll": "Show All Hidden Nodes",
7330
7350
  "contextMenu.openSource": "Open Source",
7351
+ "contextMenu.expandHere": "Expand From Here",
7352
+ "contextMenu.collapseHere": "Collapse To Here",
7353
+ "contextMenu.pinNode": "Pin Node",
7354
+ "contextMenu.unpinNode": "Unpin Node",
7355
+ "contextMenu.copyId": "Copy Symbol ID",
7356
+ "contextMenu.copied": "Copied",
7331
7357
  "a11y.graphContainer": "Code graph interactive area",
7332
7358
  "a11y.graphContainerStats": "Code graph interactive area: {viewMode}, {nodeCount} nodes, {edgeCount} edges",
7333
7359
  "a11y.selectedNode": "Selected node",
@@ -7509,7 +7535,17 @@ const en = {
7509
7535
  "views.srcAuto": "Collected by host",
7510
7536
  "views.srcQuery": "Provided by caller",
7511
7537
  "views.srcDuration": "{ms}ms",
7512
- "views.srcTruncated": "truncated"
7538
+ "views.srcTruncated": "truncated",
7539
+ "batchbar.ariaLabel": "Batch actions",
7540
+ "batchbar.count": "{count} selected",
7541
+ "batchbar.expand": "Expand",
7542
+ "batchbar.expandAria": "Expand all selected nodes",
7543
+ "batchbar.collapse": "Collapse",
7544
+ "batchbar.collapseAria": "Collapse all selected nodes",
7545
+ "batchbar.export": "Export JSON",
7546
+ "batchbar.exportAria": "Export selected nodes as JSON",
7547
+ "batchbar.clear": "Clear",
7548
+ "batchbar.clearAria": "Clear multi-selection"
7513
7549
  };
7514
7550
 
7515
7551
  //#endregion
@@ -7997,6 +8033,7 @@ const zh = {
7997
8033
  "canvasControls.shortcutZoomIn": "+ 放大",
7998
8034
  "canvasControls.shortcutZoomOut": "- 缩小",
7999
8035
  "canvasControls.shortcutFit": "F 适应窗口",
8036
+ "canvasControls.shortcutBoxSelect": "Shift+拖拽 框选节点",
8000
8037
  "canvasControls.multiSelect": "多选模式",
8001
8038
  "micro.closeView": "关闭微观视图",
8002
8039
  "micro.functionBar": "微观视图工具栏",
@@ -8160,6 +8197,12 @@ const zh = {
8160
8197
  "contextMenu.findPath": "从此节点查找路径",
8161
8198
  "contextMenu.showAll": "显示所有隐藏节点",
8162
8199
  "contextMenu.openSource": "打开源码",
8200
+ "contextMenu.expandHere": "从此展开",
8201
+ "contextMenu.collapseHere": "收起到这里",
8202
+ "contextMenu.pinNode": "固定节点",
8203
+ "contextMenu.unpinNode": "取消固定",
8204
+ "contextMenu.copyId": "复制符号 ID",
8205
+ "contextMenu.copied": "已复制",
8163
8206
  "a11y.graphContainer": "代码图谱交互区域",
8164
8207
  "a11y.graphContainerStats": "代码图谱交互区域: {viewMode}, {nodeCount} 节点, {edgeCount} 边",
8165
8208
  "a11y.selectedNode": "已选中节点",
@@ -8341,7 +8384,17 @@ const zh = {
8341
8384
  "views.srcAuto": "宿主自动采集",
8342
8385
  "views.srcQuery": "调用方传入",
8343
8386
  "views.srcDuration": "耗时 {ms}ms",
8344
- "views.srcTruncated": "已截断"
8387
+ "views.srcTruncated": "已截断",
8388
+ "batchbar.ariaLabel": "批量操作条",
8389
+ "batchbar.count": "已选 {count} 个",
8390
+ "batchbar.expand": "展开",
8391
+ "batchbar.expandAria": "批量展开选中的节点",
8392
+ "batchbar.collapse": "收起",
8393
+ "batchbar.collapseAria": "批量收起选中的节点",
8394
+ "batchbar.export": "导出 JSON",
8395
+ "batchbar.exportAria": "导出选中节点为 JSON 文件",
8396
+ "batchbar.clear": "清除",
8397
+ "batchbar.clearAria": "清除多选"
8345
8398
  };
8346
8399
 
8347
8400
  //#endregion
@@ -9349,14 +9402,14 @@ function etagKeyForEdges(fromId, scope, cursor, limit$1, detail) {
9349
9402
 
9350
9403
  //#endregion
9351
9404
  //#region src/client/api/requestCore.ts
9352
- const log$40 = scoped("client");
9405
+ const log$41 = scoped("client");
9353
9406
  const DEFAULT_TIMEOUT = API.DEFAULT_TIMEOUT_MS;
9354
9407
  async function withAbortGuard(name$1, run$3, fallback, logContext) {
9355
9408
  try {
9356
9409
  return await run$3();
9357
9410
  } catch (e) {
9358
9411
  if (e instanceof DOMException && e.name === "AbortError") return fallback;
9359
- log$40.warn(`${name$1} failed`, {
9412
+ log$41.warn(`${name$1} failed`, {
9360
9413
  ...logContext,
9361
9414
  error: e instanceof Error ? e.message : String(e)
9362
9415
  });
@@ -9689,7 +9742,7 @@ async function fetchPathEnumerate(source, target, options = {}) {
9689
9742
 
9690
9743
  //#endregion
9691
9744
  //#region src/client/api/index.ts
9692
- const log$39 = scoped("client");
9745
+ const log$40 = scoped("client");
9693
9746
  /** 构造一个空 sink;调用方先建后传,取数函数负责填充。 */
9694
9747
  function createEtagSink() {
9695
9748
  return {
@@ -9890,7 +9943,7 @@ async function fetchGraphDataIncremental(clientEtag, scopeRepoId) {
9890
9943
  const data$2 = coerceGraphData(await res.json());
9891
9944
  if (!data$2) return null;
9892
9945
  if (isForeignRepo(data$2.metadata.repoId, scopeRepoId)) {
9893
- log$39.info("incremental graph fetch ignored for another workspace", {
9946
+ log$40.info("incremental graph fetch ignored for another workspace", {
9894
9947
  repoId: data$2.metadata.repoId,
9895
9948
  activeRepoId: scopeRepoId
9896
9949
  });
@@ -10031,7 +10084,7 @@ async function fetchEdges(fromId, scope = "file", cursor, limit$1, signal, works
10031
10084
 
10032
10085
  //#endregion
10033
10086
  //#region src/client/diagram/index.ts
10034
- const log$38 = scoped("diagram");
10087
+ const log$39 = scoped("diagram");
10035
10088
  const macroAssemblerRegistry = new Map([
10036
10089
  ["inheritance", new InheritanceAssembler()],
10037
10090
  ["callgraph", new CallGraphAssembler()],
@@ -10231,7 +10284,7 @@ async function assembleMicroDiagram(viewMode, filePath, functionName, workspace)
10231
10284
  body: JSON.stringify({ filePath })
10232
10285
  });
10233
10286
  } catch (e) {
10234
- log$38.warn("AST fetch failed", e);
10287
+ log$39.warn("AST fetch failed", e);
10235
10288
  throw new Error(`Network error: unable to reach AST endpoint`);
10236
10289
  }
10237
10290
  if (!response.ok) {
@@ -10246,7 +10299,7 @@ async function assembleMicroDiagram(viewMode, filePath, functionName, workspace)
10246
10299
  const parsed = JSON.parse(errorBody);
10247
10300
  serverMsg = parsed.error || "";
10248
10301
  } catch (e) {
10249
- log$38.warn("AST error body parse failed", e);
10302
+ log$39.warn("AST error body parse failed", e);
10250
10303
  serverMsg = errorBody;
10251
10304
  }
10252
10305
  throw new Error(serverMsg || `AST parsing failed for "${filePath}"`);
@@ -10381,7 +10434,7 @@ function createEdgeSlice(_set, _get) {
10381
10434
  nodeEdgeTotals: totals$1,
10382
10435
  nodeEdgeCursors: cursors$1
10383
10436
  });
10384
- log$42.info("node edges loaded (cache)", {
10437
+ log$43.info("node edges loaded (cache)", {
10385
10438
  nodeId,
10386
10439
  scope,
10387
10440
  edges: data$3.edges.length
@@ -10394,7 +10447,7 @@ function createEdgeSlice(_set, _get) {
10394
10447
  const data$2 = await fetchEdges(nodeId, scope, cursor, EDGE_DETAIL.PAGE_SIZE, controller.signal, _get().currentWorkspace, detail, sink);
10395
10448
  if (!isCurrent() || gen !== internals.expandGeneration) return;
10396
10449
  if (sink.notModified) {
10397
- log$42.info("node edges not modified (304)", {
10450
+ log$43.info("node edges not modified (304)", {
10398
10451
  nodeId,
10399
10452
  scope,
10400
10453
  cursor,
@@ -10403,7 +10456,7 @@ function createEdgeSlice(_set, _get) {
10403
10456
  return;
10404
10457
  }
10405
10458
  if (!data$2) {
10406
- log$42.warn("loadNodeEdges: no response", {
10459
+ log$43.warn("loadNodeEdges: no response", {
10407
10460
  nodeId,
10408
10461
  scope,
10409
10462
  cursor,
@@ -10430,7 +10483,7 @@ function createEdgeSlice(_set, _get) {
10430
10483
  edges: merged,
10431
10484
  total: data$2.total
10432
10485
  });
10433
- log$42.info("node edges loaded", {
10486
+ log$43.info("node edges loaded", {
10434
10487
  nodeId,
10435
10488
  scope,
10436
10489
  detail,
@@ -10441,7 +10494,7 @@ function createEdgeSlice(_set, _get) {
10441
10494
  });
10442
10495
  commitEtag(sink);
10443
10496
  } catch (err) {
10444
- if (!(err instanceof DOMException && err.name === "AbortError")) log$42.warn("loadNodeEdges failed", {
10497
+ if (!(err instanceof DOMException && err.name === "AbortError")) log$43.warn("loadNodeEdges failed", {
10445
10498
  nodeId,
10446
10499
  scope,
10447
10500
  err
@@ -10564,7 +10617,7 @@ function createGraphDataSlice(_set, _get) {
10564
10617
  ...emptyDiffState()
10565
10618
  });
10566
10619
  } catch (err) {
10567
- log$42.error("setGraphData failed", { err });
10620
+ log$43.error("setGraphData failed", { err });
10568
10621
  _set({
10569
10622
  error: err instanceof Error ? err.message : String(err),
10570
10623
  isLoading: false
@@ -10633,12 +10686,12 @@ function createGraphDataSlice(_set, _get) {
10633
10686
  ...emptyEdgeState(),
10634
10687
  ...emptyDiffState()
10635
10688
  });
10636
- log$42.info("bootstrap data set", {
10689
+ log$43.info("bootstrap data set", {
10637
10690
  rootNodes: sanitized.nodes.length,
10638
10691
  totalNodeCount: metadata.totalNodeCount
10639
10692
  });
10640
10693
  } catch (err) {
10641
- log$42.error("setBootstrapData failed", { err });
10694
+ log$43.error("setBootstrapData failed", { err });
10642
10695
  _set({
10643
10696
  error: err instanceof Error ? err.message : String(err),
10644
10697
  isLoading: false
@@ -10681,7 +10734,7 @@ function createGraphDataSlice(_set, _get) {
10681
10734
  nodes: rawNodes,
10682
10735
  edges: rawEdges
10683
10736
  }, delta, { minRetainRatio: GRAPH_DELTA_SHRINK.MIN_RETAIN_RATIO })) {
10684
- log$42.info("applyGraphDelta refused: delta does not match the current revision or would shrink the graph", {
10737
+ log$43.info("applyGraphDelta refused: delta does not match the current revision or would shrink the graph", {
10685
10738
  storedNodes: rawNodes.length,
10686
10739
  storedEdges: rawEdges.length,
10687
10740
  ...graphDeltaSize(delta)
@@ -10743,7 +10796,7 @@ function createGraphDataSlice(_set, _get) {
10743
10796
  ...emptyEdgeState(),
10744
10797
  ...emptyDiffState()
10745
10798
  });
10746
- log$42.info("applied graph delta", {
10799
+ log$43.info("applied graph delta", {
10747
10800
  addedNodes: delta.addedNodes.length,
10748
10801
  removedNodes: delta.removedNodes.length,
10749
10802
  modifiedNodes: delta.modifiedNodes.length,
@@ -10753,7 +10806,7 @@ function createGraphDataSlice(_set, _get) {
10753
10806
  });
10754
10807
  return true;
10755
10808
  } catch (err) {
10756
- log$42.error("applyGraphDelta failed", { err });
10809
+ log$43.error("applyGraphDelta failed", { err });
10757
10810
  _set({
10758
10811
  error: err instanceof Error ? err.message : String(err),
10759
10812
  isLoading: false
@@ -11099,7 +11152,7 @@ function createSessionSlice(_set, _get) {
11099
11152
 
11100
11153
  //#endregion
11101
11154
  //#region src/client/utils/urlState.ts
11102
- const log$37 = scoped("url-state");
11155
+ const log$38 = scoped("url-state");
11103
11156
  const PREFIX_DIR = "d:";
11104
11157
  const PREFIX_FILE = "f:";
11105
11158
  const PREFIX_SYMBOL = "s:";
@@ -11156,7 +11209,7 @@ function updateUrlExpand(expandedNodeIds) {
11156
11209
  if (urlString.length > URL_STATE.MAX_LENGTH) {
11157
11210
  const trimmed = expandParam.split(",").filter((p$1) => !p$1.startsWith(PREFIX_SYMBOL));
11158
11211
  url.searchParams.set("expand", trimmed.join(","));
11159
- log$37.warn("URL too long, trimmed symbol-level expansions");
11212
+ log$38.warn("URL too long, trimmed symbol-level expansions");
11160
11213
  }
11161
11214
  window.history.replaceState(null, "", url.toString());
11162
11215
  }
@@ -11251,7 +11304,7 @@ async function loadFunctionBodyAst(nodeId, signal, gen, _set, _get, cursor = 0)
11251
11304
  const data$2 = await measureAsync("ast-fetch", () => fetchSymbolAST(nodeId, signal, _get().currentWorkspace, cursor, sink));
11252
11305
  if (gen !== internals.expandGeneration || !isCurrent()) return;
11253
11306
  if (sink.notModified) {
11254
- log$42.info("symbol AST not modified (304)", {
11307
+ log$43.info("symbol AST not modified (304)", {
11255
11308
  nodeId,
11256
11309
  cursor
11257
11310
  });
@@ -11259,7 +11312,7 @@ async function loadFunctionBodyAst(nodeId, signal, gen, _set, _get, cursor = 0)
11259
11312
  }
11260
11313
  if (!data$2) return;
11261
11314
  const { astNodes: truncatedAstNodes, astEdges: truncatedAstEdges, sourceLines: truncatedSourceLines } = truncateAstData(data$2);
11262
- if (truncatedAstNodes.length < data$2.astNodes.length) log$42.warn("symbol AST truncated", {
11315
+ if (truncatedAstNodes.length < data$2.astNodes.length) log$43.warn("symbol AST truncated", {
11263
11316
  nodeId,
11264
11317
  original: data$2.astNodes.length,
11265
11318
  truncated: truncatedAstNodes.length
@@ -11279,7 +11332,7 @@ async function loadFunctionBodyAst(nodeId, signal, gen, _set, _get, cursor = 0)
11279
11332
  if (internals.persistentCache && !data$2.hasMore && cursor === 0) await internals.persistentCache.setAST(nodeId, data$2);
11280
11333
  commitEtag(sink);
11281
11334
  } catch (err) {
11282
- log$42.warn("expandNode: fetchSymbolAST failed", {
11335
+ log$43.warn("expandNode: fetchSymbolAST failed", {
11283
11336
  nodeId,
11284
11337
  err
11285
11338
  });
@@ -11306,7 +11359,7 @@ function createExpandSlice(_set, _get) {
11306
11359
  if (expandingFunctionBody) enforceFunctionBodyBudget(effectiveExpanded, rawNodes, FUNCTION_BODY.MAX_EXPANDED - 1, pinnedIds);
11307
11360
  effectiveExpanded.add(nodeId);
11308
11361
  collapseExcessClusters(nodes, effectiveExpanded, maxVisible, cost, pinnedIds);
11309
- if (cost > CLUSTER.EXPAND_BUDGET_SINGLE) log$42.warn("expand budget exceeded", {
11362
+ if (cost > CLUSTER.EXPAND_BUDGET_SINGLE) log$43.warn("expand budget exceeded", {
11310
11363
  nodeId,
11311
11364
  cost,
11312
11365
  budget: CLUSTER.EXPAND_BUDGET_SINGLE
@@ -11321,7 +11374,7 @@ function createExpandSlice(_set, _get) {
11321
11374
  const expandController = replaceController(internals.expandAbortControllers, nodeId);
11322
11375
  _get().loadNodeChildren(nodeId, { signal: expandController.signal }).catch((err) => {
11323
11376
  if (err instanceof DOMException && err.name === "AbortError") return;
11324
- log$42.warn("expandNode: loadNodeChildren failed", {
11377
+ log$43.warn("expandNode: loadNodeChildren failed", {
11325
11378
  nodeId,
11326
11379
  err
11327
11380
  });
@@ -11343,7 +11396,7 @@ function createExpandSlice(_set, _get) {
11343
11396
  fetchFileOutline(nodeId, expandController.signal, _get().currentWorkspace, sink).then(async (data$2) => {
11344
11397
  if (gen !== internals.expandGeneration) return;
11345
11398
  if (sink.notModified) {
11346
- log$42.info("file outline not modified (304)", { nodeId });
11399
+ log$43.info("file outline not modified (304)", { nodeId });
11347
11400
  return;
11348
11401
  }
11349
11402
  if (data$2 && data$2.symbols.length > 0) {
@@ -11352,7 +11405,7 @@ function createExpandSlice(_set, _get) {
11352
11405
  commitEtag(sink);
11353
11406
  }
11354
11407
  }).catch((err) => {
11355
- log$42.warn("expandNode: fetchFileOutline failed", {
11408
+ log$43.warn("expandNode: fetchFileOutline failed", {
11356
11409
  nodeId,
11357
11410
  err
11358
11411
  });
@@ -11390,7 +11443,7 @@ function createExpandSlice(_set, _get) {
11390
11443
  loadedChildrenByNodeId: newLoaded$1,
11391
11444
  pendingRequests: clearedPending$1
11392
11445
  });
11393
- log$42.info("node children not modified (304)", { nodeId });
11446
+ log$43.info("node children not modified (304)", { nodeId });
11394
11447
  return;
11395
11448
  }
11396
11449
  if (!data$2 || data$2.nodes.length === 0) {
@@ -11439,7 +11492,7 @@ function createExpandSlice(_set, _get) {
11439
11492
  loadedChildrenByNodeId: newLoaded,
11440
11493
  pendingRequests: clearedPending
11441
11494
  });
11442
- log$42.info("node children loaded", {
11495
+ log$43.info("node children loaded", {
11443
11496
  nodeId,
11444
11497
  newNodes: newNodes.length,
11445
11498
  newEdges: newEdges.length
@@ -11458,7 +11511,7 @@ function createExpandSlice(_set, _get) {
11458
11511
  pendingRequests: clearedPending,
11459
11512
  loadErrorsByNodeId: newErrors
11460
11513
  });
11461
- log$42.warn("loadNodeChildren failed", {
11514
+ log$43.warn("loadNodeChildren failed", {
11462
11515
  nodeId,
11463
11516
  err
11464
11517
  });
@@ -11469,7 +11522,7 @@ function createExpandSlice(_set, _get) {
11469
11522
  const ws = _get().currentWorkspace;
11470
11523
  const data$2 = await fetchBootstrap(void 0, ws);
11471
11524
  if (!data$2) {
11472
- log$42.info("bootstrap returned no payload, falling back to full load");
11525
+ log$43.info("bootstrap returned no payload, falling back to full load");
11473
11526
  return false;
11474
11527
  }
11475
11528
  _get().setIndexStatus(data$2.indexStatus);
@@ -11480,11 +11533,11 @@ function createExpandSlice(_set, _get) {
11480
11533
  timestamp: data$2.metadata.timestamp,
11481
11534
  graphLayer: data$2.metadata.graphLayer
11482
11535
  });
11483
- log$42.info("bootstrap: index building, empty skeleton installed", { repoId: data$2.metadata.repoId });
11536
+ log$43.info("bootstrap: index building, empty skeleton installed", { repoId: data$2.metadata.repoId });
11484
11537
  return true;
11485
11538
  }
11486
11539
  if (data$2.topLevelNodes.length === 0) {
11487
- log$42.info("bootstrap returned empty, falling back to full load");
11540
+ log$43.info("bootstrap returned empty, falling back to full load");
11488
11541
  return false;
11489
11542
  }
11490
11543
  const state = _get();
@@ -11494,14 +11547,14 @@ function createExpandSlice(_set, _get) {
11494
11547
  timestamp: data$2.metadata.timestamp,
11495
11548
  graphLayer: data$2.metadata.graphLayer
11496
11549
  });
11497
- log$42.info("bootstrap loaded", {
11550
+ log$43.info("bootstrap loaded", {
11498
11551
  rootNodes: data$2.topLevelNodes.length,
11499
11552
  totalNodeCount: data$2.metadata.totalNodeCount,
11500
11553
  indexStatus: data$2.indexStatus
11501
11554
  });
11502
11555
  return true;
11503
11556
  } catch (err) {
11504
- log$42.warn("loadBootstrap failed, falling back to full load", { err });
11557
+ log$43.warn("loadBootstrap failed, falling back to full load", { err });
11505
11558
  return false;
11506
11559
  }
11507
11560
  },
@@ -11661,7 +11714,7 @@ function createExpandSlice(_set, _get) {
11661
11714
  if (!exists) {
11662
11715
  const parentId = nodeId.includes("/") ? nodeId.substring(0, nodeId.lastIndexOf("/")) : null;
11663
11716
  if (parentId && !state.loadedChildrenByNodeId.has(parentId)) await state.loadNodeChildren(parentId, { signal: restoreController.signal }).catch((err) => {
11664
- if (!(err instanceof DOMException && err.name === "AbortError")) log$42.warn("restoreFromUrl: loadNodeChildren failed", {
11717
+ if (!(err instanceof DOMException && err.name === "AbortError")) log$43.warn("restoreFromUrl: loadNodeChildren failed", {
11665
11718
  parentId,
11666
11719
  err
11667
11720
  });
@@ -11686,7 +11739,7 @@ function createExpandSlice(_set, _get) {
11686
11739
  }
11687
11740
  internals.restoreTimeoutHandle = null;
11688
11741
  _set({ urlRestored: true });
11689
- log$42.info("URL restore completed", { restoredCount: sortedIds.length });
11742
+ log$43.info("URL restore completed", { restoredCount: sortedIds.length });
11690
11743
  }
11691
11744
  };
11692
11745
  }
@@ -11868,7 +11921,7 @@ function createAnalysisSlice(_set, _get) {
11868
11921
  isLoading: false
11869
11922
  });
11870
11923
  } catch (e) {
11871
- log$42.warn("assembleMultiFunction failed", e);
11924
+ log$43.warn("assembleMultiFunction failed", e);
11872
11925
  _set({
11873
11926
  error: t("error.callChainFailed"),
11874
11927
  isLoading: false
@@ -11954,7 +12007,7 @@ function createPanelLodSlice(_set, _get) {
11954
12007
 
11955
12008
  //#endregion
11956
12009
  //#region src/client/api/keywordQuery.ts
11957
- const log$36 = scoped("client");
12010
+ const log$37 = scoped("client");
11958
12011
  /**
11959
12012
 
11960
12013
  * 条件请求的请求键(与宿主 `computeQueryEtag` 的**参数域**对齐)。
@@ -12013,7 +12066,7 @@ async function fetchKeywordQuery(params, signal) {
12013
12066
  res = await fetchWithTimeout(`/api/v1/codegraph/query?${search.toString()}`, options);
12014
12067
  } catch (e) {
12015
12068
  const aborted = e instanceof DOMException && e.name === "AbortError";
12016
- log$36.warn("fetchKeywordQuery failed", {
12069
+ log$37.warn("fetchKeywordQuery failed", {
12017
12070
  aborted,
12018
12071
  externalAborted: signal?.aborted === true,
12019
12072
  error: e instanceof Error ? e.message : String(e)
@@ -12030,7 +12083,7 @@ async function fetchKeywordQuery(params, signal) {
12030
12083
  try {
12031
12084
  payload = await res.json();
12032
12085
  } catch (e) {
12033
- log$36.warn("fetchKeywordQuery: non-JSON body", { error: e instanceof Error ? e.message : String(e) });
12086
+ log$37.warn("fetchKeywordQuery: non-JSON body", { error: e instanceof Error ? e.message : String(e) });
12034
12087
  return {
12035
12088
  kind: "failed",
12036
12089
  status: res.status
@@ -12168,7 +12221,7 @@ function createKeywordQuerySlice(_set, _get) {
12168
12221
  });
12169
12222
  if (seq !== _get().keywordQuerySeq) return false;
12170
12223
  if (outcome.kind === "not-modified") {
12171
- log$42.info("keyword query not modified (304)", { q: trimmed });
12224
+ log$43.info("keyword query not modified (304)", { q: trimmed });
12172
12225
  const previous = _get().keywordQueryResult;
12173
12226
  _set({
12174
12227
  keywordQueryStatus: previous ? "ready" : "idle",
@@ -12217,7 +12270,7 @@ function createKeywordQuerySlice(_set, _get) {
12217
12270
  const state = _get();
12218
12271
  const exists = state.rawNodes.some((node) => node.id === nodeId);
12219
12272
  if (!exists) {
12220
- log$42.warn("selectKeywordSeed: node not in current graph", { nodeId });
12273
+ log$43.warn("selectKeywordSeed: node not in current graph", { nodeId });
12221
12274
  return;
12222
12275
  }
12223
12276
  _set({
@@ -12239,7 +12292,7 @@ function createKeywordQuerySlice(_set, _get) {
12239
12292
  ...workspace && workspace !== "." ? { workspace } : {}
12240
12293
  });
12241
12294
  if (outcome.kind !== "data") {
12242
- log$42.warn("revealRemoteNode: query failed", {
12295
+ log$43.warn("revealRemoteNode: query failed", {
12243
12296
  nodeId,
12244
12297
  kind: outcome.kind
12245
12298
  });
@@ -12247,12 +12300,12 @@ function createKeywordQuerySlice(_set, _get) {
12247
12300
  }
12248
12301
  const target = outcome.data.nodes.find((n) => n.id === nodeId);
12249
12302
  if (!target) {
12250
- log$42.warn("revealRemoteNode: payload does not contain the requested node", { nodeId });
12303
+ log$43.warn("revealRemoteNode: payload does not contain the requested node", { nodeId });
12251
12304
  return false;
12252
12305
  }
12253
12306
  mergeAndRecluster(outcome.data.nodes, outcome.data.edges, _set, "remote search hit merged", { nodeId });
12254
12307
  if (!_get().rawNodes.some((node) => node.id === nodeId)) {
12255
- log$42.warn("revealRemoteNode: node still absent after merge", { nodeId });
12308
+ log$43.warn("revealRemoteNode: node still absent after merge", { nodeId });
12256
12309
  return false;
12257
12310
  }
12258
12311
  _set({
@@ -12302,7 +12355,7 @@ function truncationReasonKey(reason) {
12302
12355
 
12303
12356
  //#endregion
12304
12357
  //#region src/client/api/pathPattern.ts
12305
- const log$35 = scoped("client");
12358
+ const log$36 = scoped("client");
12306
12359
  /**
12307
12360
  * 把宿主响应归一成 `PathPatternData`。
12308
12361
  *
@@ -12362,7 +12415,7 @@ async function fetchPathsByPattern(params, signal) {
12362
12415
  res = await fetchWithTimeout(`/api/v1/codegraph/paths?${search.toString()}`, options);
12363
12416
  } catch (e) {
12364
12417
  const aborted = e instanceof DOMException && e.name === "AbortError";
12365
- log$35.warn("fetchPathsByPattern failed", {
12418
+ log$36.warn("fetchPathsByPattern failed", {
12366
12419
  aborted,
12367
12420
  externalAborted: signal?.aborted === true,
12368
12421
  error: e instanceof Error ? e.message : String(e)
@@ -12383,7 +12436,7 @@ async function fetchPathsByPattern(params, signal) {
12383
12436
  const record$1 = body && typeof body === "object" ? body : {};
12384
12437
  const errorCode = typeof record$1.error === "string" ? record$1.error : "invalid-pattern";
12385
12438
  const diagnostic = typeof record$1.diagnostic === "string" ? record$1.diagnostic : typeof record$1.message === "string" ? record$1.message : "";
12386
- log$35.warn("fetchPathsByPattern: invalid pattern", {
12439
+ log$36.warn("fetchPathsByPattern: invalid pattern", {
12387
12440
  errorCode,
12388
12441
  diagnostic
12389
12442
  });
@@ -12401,7 +12454,7 @@ async function fetchPathsByPattern(params, signal) {
12401
12454
  try {
12402
12455
  payload = await res.json();
12403
12456
  } catch (e) {
12404
- log$35.warn("fetchPathsByPattern: non-JSON body", { error: e instanceof Error ? e.message : String(e) });
12457
+ log$36.warn("fetchPathsByPattern: non-JSON body", { error: e instanceof Error ? e.message : String(e) });
12405
12458
  return {
12406
12459
  kind: "failed",
12407
12460
  status: res.status
@@ -12430,7 +12483,7 @@ async function fetchPathPatternSyntax(pattern, signal) {
12430
12483
  res = await fetchWithTimeout(`/api/v1/codegraph/path-pattern/syntax?${search.toString()}`, options);
12431
12484
  } catch (e) {
12432
12485
  const aborted = e instanceof DOMException && e.name === "AbortError";
12433
- log$35.warn("fetchPathPatternSyntax failed", {
12486
+ log$36.warn("fetchPathPatternSyntax failed", {
12434
12487
  aborted,
12435
12488
  externalAborted: signal?.aborted === true,
12436
12489
  error: e instanceof Error ? e.message : String(e)
@@ -12453,7 +12506,7 @@ async function fetchPathPatternSyntax(pattern, signal) {
12453
12506
  try {
12454
12507
  payload = await res.json();
12455
12508
  } catch (e) {
12456
- log$35.warn("fetchPathPatternSyntax: non-JSON body", { error: e instanceof Error ? e.message : String(e) });
12509
+ log$36.warn("fetchPathPatternSyntax: non-JSON body", { error: e instanceof Error ? e.message : String(e) });
12457
12510
  return {
12458
12511
  ok: false,
12459
12512
  kind: "failed",
@@ -12539,7 +12592,7 @@ function applyOutcome(outcome, pattern, workspace, basePaths, set$1) {
12539
12592
  return false;
12540
12593
  }
12541
12594
  if (outcome.kind === "invalid") {
12542
- log$42.warn("ρ pattern rejected (invalid syntax)", {
12595
+ log$43.warn("ρ pattern rejected (invalid syntax)", {
12543
12596
  pattern,
12544
12597
  diagnostic: outcome.diagnostic
12545
12598
  });
@@ -12676,9 +12729,76 @@ function summarizePathPatternResult(result) {
12676
12729
  };
12677
12730
  }
12678
12731
 
12732
+ //#endregion
12733
+ //#region src/client/store/slices/multiSelectSlice.ts
12734
+ /**
12735
+ * 多选链路的**消费面**(cycle-149)。
12736
+ *
12737
+ * cycle-141 D 在渲染器侧交付了 Shift+Click 多选(`InteractionController.multiSelectedIds`
12738
+ * + `multi-selected` class + `onSelectionToggle` 回调),但该回调在全仓**零消费方**——
12739
+ * 多选集合只活在渲染器内部,store/UI 全都看不到。本切片把这条链路接完:
12740
+ *
12741
+ * - `multiSelectedIds` 是渲染器多选集合的**镜像**(单一事实源仍是渲染器,
12742
+ * store 只做镜像 + 批量动作的消费面;两边经 `syncMultiSelection`/`clearMultiSelection`
12743
+ * 同步,避免双集合各自演化后漂移)。
12744
+ * - `batchExpandSelected` 复用 expandSlice 的 `batchExpandNodes`(函数体预算 / pinned
12745
+ * 语义不重写——两处实现必然漂移,CLAUDE.md「Two data paths for the same semantics
12746
+ * must share ONE implementation」)。
12747
+ * - `batchCollapseSelected` 与 expandSlice `collapseAll` 同形:**单次** recluster
12748
+ * (逐个 collapseNode 会 N 次重聚类),pinned 节点不收(与 `collapseNode` 语义一致)。
12749
+ * - `exportSelectedNodes` 从 `rawNodes` 过滤导出;**聚合节点(cluster)不在 rawNodes 里**,
12750
+ * 选中聚合节点时该 id 被静默跳过——这是有意的诚实缺口(渲染器合并态 store 不可见),
12751
+ * UI 侧以「导出 N 个」计数如实呈现,不伪造。
12752
+ *
12753
+ * `multiSelectedIds` **不持久化**:画布选区是会话瞬时态,reload 后节点可能已变
12754
+ * (与 `xuanjiSignal` 同判据);`partialize` 白名单不收录即天然排除。
12755
+ */
12756
+ function createMultiSelectSlice(_set, _get) {
12757
+ return {
12758
+ multiSelectedIds: new Set(),
12759
+ syncMultiSelection: (ids) => {
12760
+ const current$2 = _get().multiSelectedIds;
12761
+ if (current$2.size === ids.length && [...ids].every((id) => current$2.has(id))) return;
12762
+ _set({ multiSelectedIds: new Set(ids) });
12763
+ },
12764
+ clearMultiSelection: () => {
12765
+ if (_get().multiSelectedIds.size === 0) return;
12766
+ _set({ multiSelectedIds: new Set() });
12767
+ },
12768
+ batchExpandSelected: () => {
12769
+ const ids = [..._get().multiSelectedIds];
12770
+ if (ids.length === 0) return 0;
12771
+ _get().batchExpandNodes(ids);
12772
+ return ids.length;
12773
+ },
12774
+ batchCollapseSelected: () => {
12775
+ const { multiSelectedIds, expandedNodeIds, expandedNodeIdsByViewMode, viewMode, pinnedIds } = _get();
12776
+ if (multiSelectedIds.size === 0) return 0;
12777
+ const newExpanded = new Set(expandedNodeIds);
12778
+ let collapsed = 0;
12779
+ for (const id of multiSelectedIds) {
12780
+ if (pinnedIds.has(id)) continue;
12781
+ if (newExpanded.delete(id)) collapsed++;
12782
+ }
12783
+ if (collapsed === 0) return 0;
12784
+ const newExpandedByViewMode = {
12785
+ ...expandedNodeIdsByViewMode,
12786
+ [viewMode]: newExpanded
12787
+ };
12788
+ reclusterWith(_get, _set, newExpanded, newExpandedByViewMode);
12789
+ return collapsed;
12790
+ },
12791
+ exportSelectedNodes: () => {
12792
+ const { multiSelectedIds, rawNodes } = _get();
12793
+ if (multiSelectedIds.size === 0) return [];
12794
+ return rawNodes.filter((n) => multiSelectedIds.has(n.id));
12795
+ }
12796
+ };
12797
+ }
12798
+
12679
12799
  //#endregion
12680
12800
  //#region src/client/utils/safeStorage.ts
12681
- const log$34 = scoped("client");
12801
+ const log$35 = scoped("client");
12682
12802
  /** 纯内存实现:同一页面的多次读写一致,刷新即丢。 */
12683
12803
  function createMemoryStorage() {
12684
12804
  const map$1 = new Map();
@@ -12736,7 +12856,7 @@ function getSafeStorage(label = "generic") {
12736
12856
  try {
12737
12857
  return local.getItem(name$1);
12738
12858
  } catch (e) {
12739
- log$34.debug("safeStorage.getItem failed", {
12859
+ log$35.debug("safeStorage.getItem failed", {
12740
12860
  label,
12741
12861
  error: e
12742
12862
  });
@@ -12747,7 +12867,7 @@ function getSafeStorage(label = "generic") {
12747
12867
  try {
12748
12868
  local.setItem(name$1, value);
12749
12869
  } catch (e) {
12750
- log$34.debug("safeStorage.setItem failed", {
12870
+ log$35.debug("safeStorage.setItem failed", {
12751
12871
  label,
12752
12872
  error: e
12753
12873
  });
@@ -12757,7 +12877,7 @@ function getSafeStorage(label = "generic") {
12757
12877
  try {
12758
12878
  local.removeItem(name$1);
12759
12879
  } catch (e) {
12760
- log$34.debug("safeStorage.removeItem failed", {
12880
+ log$35.debug("safeStorage.removeItem failed", {
12761
12881
  label,
12762
12882
  error: e
12763
12883
  });
@@ -12766,7 +12886,7 @@ function getSafeStorage(label = "generic") {
12766
12886
  };
12767
12887
  const session = isStorageUsable(() => window.sessionStorage);
12768
12888
  if (session) {
12769
- log$34.info("localStorage unavailable, falling back to sessionStorage", { label });
12889
+ log$35.info("localStorage unavailable, falling back to sessionStorage", { label });
12770
12890
  return {
12771
12891
  getItem: (name$1) => {
12772
12892
  try {
@@ -12787,7 +12907,7 @@ function getSafeStorage(label = "generic") {
12787
12907
  }
12788
12908
  };
12789
12909
  }
12790
- log$34.info("no Web Storage available, falling back to in-memory storage", { label });
12910
+ log$35.info("no Web Storage available, falling back to in-memory storage", { label });
12791
12911
  return createMemoryStorage();
12792
12912
  }
12793
12913
  /**
@@ -12848,7 +12968,8 @@ function disposeStore() {
12848
12968
  nodeEdgeTotals: new Map(),
12849
12969
  nodeEdgeCursors: new Map(),
12850
12970
  loadingEdgeNodeIds: new Set(),
12851
- detailEdgeNodeIds: new Set()
12971
+ detailEdgeNodeIds: new Set(),
12972
+ multiSelectedIds: new Set()
12852
12973
  });
12853
12974
  if (internals.restoreTimeoutHandle !== null) {
12854
12975
  clearTimeout(internals.restoreTimeoutHandle);
@@ -12887,7 +13008,8 @@ const useGraphStore = create$1()(persist((set$1, get$3) => {
12887
13008
  ...createAnalysisSlice(set$1, get$3),
12888
13009
  ...createPanelLodSlice(set$1, get$3),
12889
13010
  ...createKeywordQuerySlice(set$1, get$3),
12890
- ...createPathPatternSlice(set$1, get$3)
13011
+ ...createPathPatternSlice(set$1, get$3),
13012
+ ...createMultiSelectSlice(set$1, get$3)
12891
13013
  };
12892
13014
  registerStoreGetter(get$3);
12893
13015
  return store;
@@ -58347,7 +58469,7 @@ var OverlapRemover = class {
58347
58469
 
58348
58470
  //#endregion
58349
58471
  //#region src/client/renderer/LayoutController.ts
58350
- const log$33 = scoped("layout");
58472
+ const log$34 = scoped("layout");
58351
58473
  var LayoutController = class {
58352
58474
  cy;
58353
58475
  layoutRaf = null;
@@ -58390,17 +58512,17 @@ var LayoutController = class {
58390
58512
  this.worker?.terminate();
58391
58513
  this.worker = null;
58392
58514
  this.workerInitFailed = true;
58393
- log$33.warn("layout worker error — falling back to main-thread layout");
58515
+ log$34.warn("layout worker error — falling back to main-thread layout");
58394
58516
  this.failPendingLayout();
58395
58517
  this.retryWorkerInit();
58396
58518
  };
58397
58519
  } else {
58398
58520
  this.workerInitFailed = true;
58399
- log$33.warn("layout worker not available — using main-thread layout");
58521
+ log$34.warn("layout worker not available — using main-thread layout");
58400
58522
  }
58401
58523
  } catch (e) {
58402
58524
  this.workerInitFailed = true;
58403
- log$33.warn("layout worker init failed — using main-thread layout", e);
58525
+ log$34.warn("layout worker init failed — using main-thread layout", e);
58404
58526
  }
58405
58527
  }
58406
58528
  /** Resolve an in-flight worker layout with no positions, so the caller falls back immediately. */
@@ -58426,7 +58548,7 @@ var LayoutController = class {
58426
58548
  if (this.workerGaveUp) return;
58427
58549
  if (this.workerRetryCount >= RENDERER.WORKER_MAX_INIT_RETRIES) {
58428
58550
  this.workerGaveUp = true;
58429
- log$33.warn(`layout worker unavailable after ${RENDERER.WORKER_MAX_INIT_RETRIES} retries — main-thread layout for the rest of this session`);
58551
+ log$34.warn(`layout worker unavailable after ${RENDERER.WORKER_MAX_INIT_RETRIES} retries — main-thread layout for the rest of this session`);
58430
58552
  return;
58431
58553
  }
58432
58554
  this.workerRetryCount++;
@@ -58447,13 +58569,13 @@ var LayoutController = class {
58447
58569
  const timeoutMs = this.workerReady ? RENDERER.LAYOUT_TIMEOUT_MS : RENDERER.WORKER_HANDSHAKE_TIMEOUT_MS;
58448
58570
  return new Promise((resolve) => {
58449
58571
  this.workerTimeout = setTimeout(() => {
58450
- log$33.warn("layout worker timeout — falling back to main-thread layout");
58572
+ log$34.warn("layout worker timeout — falling back to main-thread layout");
58451
58573
  this.failPendingLayout();
58452
58574
  if (this.worker) {
58453
58575
  this.worker.terminate();
58454
58576
  this.worker = null;
58455
58577
  this.workerInitFailed = true;
58456
- log$33.warn("layout worker terminated due to timeout — retrying within the retry budget");
58578
+ log$34.warn("layout worker terminated due to timeout — retrying within the retry budget");
58457
58579
  this.retryWorkerInit();
58458
58580
  }
58459
58581
  }, timeoutMs);
@@ -58527,7 +58649,7 @@ var LayoutController = class {
58527
58649
  try {
58528
58650
  this.runIncrementalLayout(layout$2, newNodeIds);
58529
58651
  } catch (e) {
58530
- log$33.warn("incremental layout failed — keeping the previous positions", e);
58652
+ log$34.warn("incremental layout failed — keeping the previous positions", e);
58531
58653
  } finally {
58532
58654
  this.layoutRaf = null;
58533
58655
  }
@@ -59291,7 +59413,7 @@ function detectCyclesDFS(adjacency, maxCycles = MAX_CYCLES_DEFAULT) {
59291
59413
 
59292
59414
  //#endregion
59293
59415
  //#region src/client/renderer/CycleDetector.ts
59294
- const log$32 = scoped("cycle-detector");
59416
+ const log$33 = scoped("cycle-detector");
59295
59417
  var CycleDetector = class {
59296
59418
  cy;
59297
59419
  adjacencyCache;
@@ -59333,7 +59455,7 @@ var CycleDetector = class {
59333
59455
  const cycles = new Set();
59334
59456
  const nodeCount = this.cy.nodes().length;
59335
59457
  if (nodeCount > CYCLE_DETECTION.MAX_NODES_FOR_DETECTION) {
59336
- log$32.warn("cycle detection skipped: graph too large", { nodeCount });
59458
+ log$33.warn("cycle detection skipped: graph too large", { nodeCount });
59337
59459
  return cycles;
59338
59460
  }
59339
59461
  const adjacencySet = this.getForwardAdjacency();
@@ -59346,7 +59468,7 @@ var CycleDetector = class {
59346
59468
  detectCyclesDetailed() {
59347
59469
  const nodeCount = this.cy.nodes().length;
59348
59470
  if (nodeCount > CYCLE_DETECTION.MAX_NODES_FOR_DETECTION) {
59349
- log$32.warn("detailed cycle detection skipped: graph too large", { nodeCount });
59471
+ log$33.warn("detailed cycle detection skipped: graph too large", { nodeCount });
59350
59472
  return [];
59351
59473
  }
59352
59474
  const adjacencySet = this.getForwardAdjacency();
@@ -59910,7 +60032,7 @@ function pushId(map$1, key, edgeId) {
59910
60032
 
59911
60033
  //#endregion
59912
60034
  //#region src/client/renderer/DataController.ts
59913
- const log$31 = scoped("data-controller");
60035
+ const log$32 = scoped("data-controller");
59914
60036
  /** 缺省结果:任何未表态路径都必须回到「必须失效」的安全方向。 */
59915
60037
  const DIRTY = { kind: "dirty" };
59916
60038
  var DataController = class {
@@ -60168,7 +60290,7 @@ var DataController = class {
60168
60290
  expectedVersion: cache$1.getVersion()
60169
60291
  };
60170
60292
  } catch (err) {
60171
- log$31.warn("adjacency reconcile failed — invalidating cache", { err: String(err) });
60293
+ log$32.warn("adjacency reconcile failed — invalidating cache", { err: String(err) });
60172
60294
  cache$1.invalidate();
60173
60295
  return DIRTY;
60174
60296
  }
@@ -60234,7 +60356,7 @@ var DataController = class {
60234
60356
  }
60235
60357
  addEdge(e) {
60236
60358
  if (this.cy.getElementById(e.source).empty() || this.cy.getElementById(e.target).empty()) {
60237
- log$31.warn("edge dropped: missing endpoint", {
60359
+ log$32.warn("edge dropped: missing endpoint", {
60238
60360
  edgeId: e.id,
60239
60361
  source: e.source,
60240
60362
  target: e.target
@@ -60397,7 +60519,7 @@ var DataController = class {
60397
60519
 
60398
60520
  //#endregion
60399
60521
  //#region src/client/renderer/FilterController.ts
60400
- const log$30 = scoped("filter");
60522
+ const log$31 = scoped("filter");
60401
60523
  var FilterController = class {
60402
60524
  cy;
60403
60525
  fanInCache = null;
@@ -60554,7 +60676,7 @@ var FilterController = class {
60554
60676
  if (/(\+|\*|\{)\{/.test(pattern)) return null;
60555
60677
  return new RegExp(pattern);
60556
60678
  } catch (e) {
60557
- log$30.warn("invalid regex pattern", e);
60679
+ log$31.warn("invalid regex pattern", e);
60558
60680
  return null;
60559
60681
  }
60560
60682
  }
@@ -60722,7 +60844,7 @@ var Trie = class {
60722
60844
 
60723
60845
  //#endregion
60724
60846
  //#region src/client/renderer/SearchController.ts
60725
- const log$29 = scoped("search");
60847
+ const log$30 = scoped("search");
60726
60848
  const BUILD_CHUNK_SIZE = 200;
60727
60849
  const BUILD_CHUNK_DELAY = 16;
60728
60850
  function requestIdleCallbackCompat(cb) {
@@ -60764,7 +60886,7 @@ var SearchController = class {
60764
60886
  this.trieBuilt = true;
60765
60887
  this.indexBuilding = false;
60766
60888
  this.buildHandle = null;
60767
- log$29.info(`Trie index built: ${entries.length} nodes`);
60889
+ log$30.info(`Trie index built: ${entries.length} nodes`);
60768
60890
  }
60769
60891
  };
60770
60892
  processChunk();
@@ -61535,10 +61657,7 @@ var InteractionController = class InteractionController {
61535
61657
  this.callbacks.onSelectionToggle?.(id, selected, [...this.multiSelectedIds]);
61536
61658
  this.callbacks.onNodeTap?.(id);
61537
61659
  } else {
61538
- if (this.multiSelectedIds.size > 0) {
61539
- this.multiSelectedIds.clear();
61540
- this.cy.nodes().removeClass("multi-selected");
61541
- }
61660
+ this.clearMultiSelect();
61542
61661
  this.callbacks.onNodeTap?.(id);
61543
61662
  }
61544
61663
  this.focusNodeId = id;
@@ -61589,6 +61708,14 @@ var InteractionController = class InteractionController {
61589
61708
  y: pos.y
61590
61709
  });
61591
61710
  });
61711
+ this.cy.on("boxend", () => {
61712
+ const ids = this.cy.$(":selected").nodes().map((n) => n.id());
61713
+ for (const id of ids) this.multiSelectedIds.add(id);
61714
+ this.cy.nodes().forEach((n) => {
61715
+ if (this.multiSelectedIds.has(n.id())) n.addClass("multi-selected");
61716
+ });
61717
+ this.callbacks.onSelectionToggle?.(ids[0] ?? "", ids.length > 0, [...this.multiSelectedIds]);
61718
+ });
61592
61719
  }
61593
61720
  /**
61594
61721
  * 绑定画布容器上的键盘 / 焦点事件。
@@ -61737,6 +61864,20 @@ var InteractionController = class InteractionController {
61737
61864
  this.callbacks = callbacks;
61738
61865
  }
61739
61866
  /**
61867
+ * 清空多选集合(cycle-149):集合 + `multi-selected` class + 通知消费方一体完成。
61868
+ *
61869
+ * 两个触发方共用本方法(单一实现,防漂移):普通点击的静默清空、
61870
+ * 批量操作条「清除」按钮经渲染器 `clearMultiSelect` 的显式清空。
61871
+ * `onSelectionClear` 让 store 镜像同步——缺了它消费方留下幽灵计数
61872
+ * (「画布无高亮但 BatchActionBar 还在」)。
61873
+ */
61874
+ clearMultiSelect() {
61875
+ if (this.multiSelectedIds.size === 0) return;
61876
+ this.multiSelectedIds.clear();
61877
+ this.cy.nodes().removeClass("multi-selected");
61878
+ this.callbacks.onSelectionClear?.();
61879
+ }
61880
+ /**
61740
61881
  * 切换节点 / 边标签的可见性(cycle-141 / D)。
61741
61882
  *
61742
61883
  * 实现放在本控制器而不是 `StyleController`:标签显隐是**交互态**而非主题态,
@@ -61768,6 +61909,7 @@ var InteractionController = class InteractionController {
61768
61909
  this.cy.off("mouseover", "edge");
61769
61910
  this.cy.off("mouseout", "edge");
61770
61911
  this.cy.off("cxttap", "node");
61912
+ this.cy.off("boxend");
61771
61913
  }
61772
61914
  };
61773
61915
 
@@ -62265,7 +62407,7 @@ var VirtualViewportController = class {
62265
62407
 
62266
62408
  //#endregion
62267
62409
  //#region src/client/renderer/CytoscapeRenderer.ts
62268
- const log$28 = scoped("cytoscape-renderer");
62410
+ const log$29 = scoped("cytoscape-renderer");
62269
62411
  function ensureExtensionsRegistered() {
62270
62412
  const cy = cytoscape$1;
62271
62413
  if (!cy.__dagreRegistered) {
@@ -62300,7 +62442,7 @@ var CytoscapeRenderer = class {
62300
62442
  try {
62301
62443
  return fn$7();
62302
62444
  } catch (err) {
62303
- log$28.error(`${name$1} failed`, err);
62445
+ log$29.error(`${name$1} failed`, err);
62304
62446
  this.lastError = err instanceof Error ? err : new Error(String(err));
62305
62447
  return fallback;
62306
62448
  }
@@ -62309,7 +62451,7 @@ var CytoscapeRenderer = class {
62309
62451
  try {
62310
62452
  fn$7();
62311
62453
  } catch (err) {
62312
- log$28.error(`${name$1} failed`, err);
62454
+ log$29.error(`${name$1} failed`, err);
62313
62455
  this.lastError = err instanceof Error ? err : new Error(String(err));
62314
62456
  }
62315
62457
  }
@@ -62356,7 +62498,9 @@ var CytoscapeRenderer = class {
62356
62498
  onNodeHoverOut: this.options.onNodeHoverOut ?? (() => {}),
62357
62499
  onEdgeHover: this.options.onEdgeHover ?? (() => {}),
62358
62500
  onEdgeHoverOut: this.options.onEdgeHoverOut ?? (() => {}),
62359
- ...this.options.onNodeContextTap ? { onNodeContextTap: this.options.onNodeContextTap } : {}
62501
+ ...this.options.onNodeContextTap ? { onNodeContextTap: this.options.onNodeContextTap } : {},
62502
+ onSelectionToggle: this.options.onSelectionToggle ?? (() => {}),
62503
+ onSelectionClear: this.options.onSelectionClear
62360
62504
  });
62361
62505
  this.pathFinder = new PathFinder(cy, this.adjacencyCache);
62362
62506
  this.virtualViewportCtrl = new VirtualViewportController(cy);
@@ -62435,27 +62579,16 @@ var CytoscapeRenderer = class {
62435
62579
  this.layoutCtrl?.applyWorkerLayout(positions);
62436
62580
  }
62437
62581
  /**
62438
-
62439
62582
  * 开启虚拟视口(视口外的节点不渲染)。
62440
-
62441
62583
  *
62442
-
62443
62584
  * 这里额外往容器上写一个 `data-cg-virtual-scroll` 标记,原因是这个特性在 DOM 上
62444
-
62445
62585
  * **完全不可观测**:它只改 cytoscape 内部的 `display` 状态,元素还在、canvas 还在,
62446
-
62447
62586
  * 任何「元素可见」类断言在开启与未开启时结果相同。cycle-93 之前 E2E 的 J35
62448
-
62449
62587
  * 因此只能断言「容器可见」,零鉴别力。
62450
-
62451
62588
  *
62452
-
62453
62589
  * 标记沿用本仓库既有的 `data-cg-*` 集成标记约定(`useGraphDerivedValues.ts:71` 的
62454
-
62455
62590
  * `data-cg-integration`)。写成「只有真的启用成功才标记」,避免
62456
-
62457
62591
  * `virtualViewportCtrl` 为 null 时留下一个骗人的标记。
62458
-
62459
62592
  */
62460
62593
  enableVirtualScroll() {
62461
62594
  this.virtualViewportCtrl?.enable();
@@ -62502,17 +62635,11 @@ var CytoscapeRenderer = class {
62502
62635
  this.viewportCtrl?.focusOnNode(nodeId);
62503
62636
  }
62504
62637
  /**
62505
-
62506
62638
  * Bring a node the user explicitly asked for (a search hit, a host symbol link) into view even
62507
-
62508
62639
  * when it is currently hidden, then centre the viewport on it. Returns `false` when the node is
62509
-
62510
62640
  * not in the rendered graph at all, or when it is excluded by an active type/connection filter —
62511
-
62512
62641
  * silently panning to an element the filter keeps invisible is what made "search a hidden node"
62513
-
62514
62642
  * look like a no-op.
62515
-
62516
62643
  */
62517
62644
  revealNode(nodeId) {
62518
62645
  return this.safeCall("revealNode", () => {
@@ -62536,15 +62663,10 @@ var CytoscapeRenderer = class {
62536
62663
  return el.data("typeFiltered") === true || el.data("connectionFiltered") === true;
62537
62664
  }
62538
62665
  /**
62539
-
62540
62666
  * Viewport-space position used to anchor the context menu when the user opens it without a
62541
-
62542
62667
  * pointer. Cytoscape keeps reporting the last computed position for `display: none` elements,
62543
-
62544
62668
  * and the menu clamps itself into the viewport, so only a node that is really absent returns
62545
-
62546
62669
  * `null`. Cytoscape 的渲染坐标相对画布容器,这里叠加容器原点换算成视口坐标。
62547
-
62548
62670
  */
62549
62671
  getNodeRenderedPosition(nodeId) {
62550
62672
  return this.safeCall("getNodeRenderedPosition", () => {
@@ -62595,6 +62717,10 @@ var CytoscapeRenderer = class {
62595
62717
  focusNeighborhood(nodeId) {
62596
62718
  this.safeCallVoid("focusNeighborhood", () => this.viewportCtrl?.focusNeighborhood(nodeId));
62597
62719
  }
62720
+ /** 清空多选集合(cycle-149):委托 InteractionController(集合+class+回调一体)。 */
62721
+ clearMultiSelect() {
62722
+ this.safeCallVoid("clearMultiSelect", () => this.interactionCtrl?.clearMultiSelect());
62723
+ }
62598
62724
  getThumbnail() {
62599
62725
  return this.viewportCtrl?.getThumbnail() ?? null;
62600
62726
  }
@@ -62664,7 +62790,7 @@ var CytoscapeRenderer = class {
62664
62790
  try {
62665
62791
  fn$7();
62666
62792
  } catch (err) {
62667
- log$28.warn(`destroy: ${label} cleanup failed`, err);
62793
+ log$29.warn(`destroy: ${label} cleanup failed`, err);
62668
62794
  }
62669
62795
  };
62670
62796
  safeDestroy("interactionCtrl", () => this.interactionCtrl?.destroy());
@@ -62758,21 +62884,13 @@ var CytoscapeRenderer = class {
62758
62884
  });
62759
62885
  }
62760
62886
  /**
62761
-
62762
62887
  * diff 标记的渲染器入口(研究报告 §六 #13)。
62763
-
62764
62888
  *
62765
-
62766
62889
  * 实现全部委托给 `HighlightController`:它已经持有 `cy` 并且是「视图态 class」的归属地,
62767
-
62768
62890
  * 把标记逻辑写两份必然漂移(`markDeadCode` 就是直接写在这里的,代价是它没有复用任何
62769
-
62770
62891
  * 控制器设施)。渲染器本身只做 `safeCall` 包裹与「控制器不存在时给空统计」的兜底——
62771
-
62772
62892
  * 后者很重要:控制器为 `null` 时若返回 `undefined`,调用方读 `.counts.added` 会抛错,
62773
-
62774
62893
  * 而这条路径在「渲染器尚未完成 init」时是真实可达的。
62775
-
62776
62894
  */
62777
62895
  applyDiffMarkers(index) {
62778
62896
  const empty = {
@@ -62796,7 +62914,7 @@ var CytoscapeRenderer = class {
62796
62914
  this.cy.nodes().stop();
62797
62915
  this.cy.edges().stop();
62798
62916
  } catch (e) {
62799
- log$28.warn("pauseAnimations failed", e);
62917
+ log$29.warn("pauseAnimations failed", e);
62800
62918
  }
62801
62919
  }
62802
62920
  resumeAnimations() {
@@ -62804,7 +62922,7 @@ var CytoscapeRenderer = class {
62804
62922
  try {
62805
62923
  this.cy.resize();
62806
62924
  } catch (e) {
62807
- log$28.warn("resumeAnimations failed", e);
62925
+ log$29.warn("resumeAnimations failed", e);
62808
62926
  }
62809
62927
  }
62810
62928
  /** 缩放画布:factor > 1 放大,factor < 1 缩小。带 200ms 动画过渡。 */
@@ -62843,7 +62961,7 @@ var CytoscapeRenderer = class {
62843
62961
 
62844
62962
  //#endregion
62845
62963
  //#region src/client/hooks/useGraphRenderer.ts
62846
- const log$27 = scoped("renderer-hook");
62964
+ const log$28 = scoped("renderer-hook");
62847
62965
  function useGraphRenderer(config) {
62848
62966
  const { data: data$2, display, interaction, filters, analysis, callbacks, showCallChainRef } = config;
62849
62967
  const { nodes, edges, rawNodes, repoId } = data$2;
@@ -62885,17 +63003,19 @@ function useGraphRenderer(config) {
62885
63003
  onNodeHoverOut: () => callbacksRef.current.onNodeHoverOut(),
62886
63004
  onEdgeTap: (edgeId, pos) => callbacksRef.current.onEdgeTap(edgeId, pos),
62887
63005
  onEdgeHover: (edgeId, pos) => callbacksRef.current.onEdgeHover(edgeId, pos),
62888
- onEdgeHoverOut: () => callbacksRef.current.onEdgeHoverOut()
63006
+ onEdgeHoverOut: () => callbacksRef.current.onEdgeHoverOut(),
63007
+ onSelectionToggle: (nodeId, selected, selectedIds) => callbacksRef.current.onSelectionToggle?.(nodeId, selected, selectedIds),
63008
+ onSelectionClear: () => callbacksRef.current.onSelectionClear?.()
62889
63009
  });
62890
63010
  renderer$1.init();
62891
63011
  rendererRef.current = renderer$1;
62892
- log$27.info("renderer initialized", {
63012
+ log$28.info("renderer initialized", {
62893
63013
  theme,
62894
63014
  w,
62895
63015
  h
62896
63016
  });
62897
63017
  } catch (err) {
62898
- log$27.error("renderer init failed", { err });
63018
+ log$28.error("renderer init failed", { err });
62899
63019
  renderer$1?.destroy();
62900
63020
  renderer$1 = null;
62901
63021
  useGraphStore.getState().setError(err instanceof Error ? err.message : String(err));
@@ -63143,7 +63263,7 @@ function useGraphRenderer(config) {
63143
63263
  a.download = `codegraph-${Date.now()}.${extension$1}`;
63144
63264
  a.click();
63145
63265
  setTimeout(() => URL.revokeObjectURL(url), UI.URL_REVOKE_DELAY_MS);
63146
- log$27.info(`exported ${format}`);
63266
+ log$28.info(`exported ${format}`);
63147
63267
  }
63148
63268
  }, [theme]);
63149
63269
  const updateTheme = useCallback((newTheme) => {
@@ -63279,7 +63399,7 @@ function useFocusMode(opts) {
63279
63399
 
63280
63400
  //#endregion
63281
63401
  //#region src/client/hooks/useNodeInteraction.ts
63282
- const log$26 = scoped("panel");
63402
+ const log$27 = scoped("panel");
63283
63403
  function useNodeInteraction(opts) {
63284
63404
  const { setSelectedNode, setSelectedNodeData, getDataRef, adjacencyMap, setTooltip, expandedNodeIds, collapseNode, expandNode, handleSelectFunction, setFocusNodeId, handleNodeDoubleTapRef } = opts;
63285
63405
  const handleNodeTap = useCallback((id) => {
@@ -63288,7 +63408,7 @@ function useNodeInteraction(opts) {
63288
63408
  const data$2 = getDataRef.current?.getNodeData(id) ?? null;
63289
63409
  setSelectedNodeData(data$2);
63290
63410
  } catch (err) {
63291
- log$26.error("handleNodeTap failed", {
63411
+ log$27.error("handleNodeTap failed", {
63292
63412
  id,
63293
63413
  err
63294
63414
  });
@@ -63320,7 +63440,7 @@ function useNodeInteraction(opts) {
63320
63440
  });
63321
63441
  }
63322
63442
  } catch (err) {
63323
- log$26.error("handleNodeHover failed", {
63443
+ log$27.error("handleNodeHover failed", {
63324
63444
  nodeId,
63325
63445
  err
63326
63446
  });
@@ -63371,7 +63491,7 @@ function useNodeInteraction(opts) {
63371
63491
  });
63372
63492
  }
63373
63493
  } catch (err) {
63374
- log$26.error("handleEdgeTap failed", {
63494
+ log$27.error("handleEdgeTap failed", {
63375
63495
  edgeId,
63376
63496
  err
63377
63497
  });
@@ -63398,7 +63518,7 @@ function useNodeInteraction(opts) {
63398
63518
  return nodeId;
63399
63519
  });
63400
63520
  } catch (err) {
63401
- log$26.error("handleNodeDoubleTap failed", {
63521
+ log$27.error("handleNodeDoubleTap failed", {
63402
63522
  nodeId,
63403
63523
  err
63404
63524
  });
@@ -63764,7 +63884,7 @@ function useDeadCodeMarker({ showDeadCode, deadCodeResult, rendererRef }) {
63764
63884
 
63765
63885
  //#endregion
63766
63886
  //#region src/client/hooks/useDiffComparison.ts
63767
- const log$25 = scoped("diff");
63887
+ const log$26 = scoped("diff");
63768
63888
  /**
63769
63889
 
63770
63890
  * 版本对比的编排层(研究报告 §六 #13)。
@@ -63857,7 +63977,7 @@ function useDiffComparison(params) {
63857
63977
  return next$1;
63858
63978
  } catch (e) {
63859
63979
  if (!aliveRef.current || gen !== generationRef.current) return null;
63860
- log$25.warn("diff comparison failed", e);
63980
+ log$26.warn("diff comparison failed", e);
63861
63981
  setError(e instanceof Error ? e.message : String(e));
63862
63982
  setIsComparing(false);
63863
63983
  return null;
@@ -64364,19 +64484,12 @@ const KEYBOARD_SHORTCUTS = [
64364
64484
  }
64365
64485
  ];
64366
64486
  /**
64367
-
64368
64487
  * 画布级快捷键(cycle-141 / D)。
64369
-
64370
64488
  *
64371
-
64372
64489
  * 与 `KEYBOARD_SHORTCUTS` 分开声明,**不注册全局监听**:面板级快捷键挂在
64373
-
64374
64490
  * `document` 上、任何焦点下都生效;画布级只在 `.cg-graph-container` 聚焦时由
64375
-
64376
64491
  * `InteractionController` 接管。此处仅作为键盘提示浮层与文档的元数据,
64377
-
64378
64492
  * 避免方向键劫持页面滚动、也避免 `usePanelKeyboard` 的测试长度断言被悄悄改变。
64379
-
64380
64493
  */
64381
64494
  const CANVAS_KEYBOARD_SHORTCUTS = [
64382
64495
  {
@@ -64414,6 +64527,10 @@ const CANVAS_KEYBOARD_SHORTCUTS = [
64414
64527
  {
64415
64528
  key: "F",
64416
64529
  descKey: "canvasControls.shortcutFit"
64530
+ },
64531
+ {
64532
+ key: "Shift+Drag",
64533
+ descKey: "canvasControls.shortcutBoxSelect"
64417
64534
  }
64418
64535
  ];
64419
64536
  function formatShortcutKey(s) {
@@ -64477,7 +64594,7 @@ function usePanelKeyboard(layout$2, setLayout, rendererRef, handlers) {
64477
64594
 
64478
64595
  //#endregion
64479
64596
  //#region src/client/hooks/useViewMode.ts
64480
- const log$24 = scoped("viewMode");
64597
+ const log$25 = scoped("viewMode");
64481
64598
  const MICRO_MODES = [
64482
64599
  "cfg",
64483
64600
  "flowchart",
@@ -64520,7 +64637,7 @@ function useViewMode(viewMode, currentWorkspace, depsFetchAbortRef) {
64520
64637
  if (controller.signal.aborted) return;
64521
64638
  if (!data$2.nodes || data$2.nodes.length === 0) return;
64522
64639
  if (isForeignRepo(data$2.metadata?.repoId, requestedWorkspace)) {
64523
- log$24.info("dependency graph ignored for another workspace", {
64640
+ log$25.info("dependency graph ignored for another workspace", {
64524
64641
  repoId: data$2.metadata?.repoId,
64525
64642
  requestedWorkspace,
64526
64643
  activeWorkspace: currentWorkspace
@@ -64545,7 +64662,7 @@ function useViewMode(viewMode, currentWorkspace, depsFetchAbortRef) {
64545
64662
  else setError(t$1("error.loadGraphData"));
64546
64663
  setLoading(false);
64547
64664
  }).catch((e) => {
64548
- log$24.warn("loadBootstrap failed", e);
64665
+ log$25.warn("loadBootstrap failed", e);
64549
64666
  setError(t$1("error.loadGraph"));
64550
64667
  setLoading(false);
64551
64668
  });
@@ -64574,7 +64691,7 @@ function useViewMode(viewMode, currentWorkspace, depsFetchAbortRef) {
64574
64691
  }).then((data$2) => {
64575
64692
  if (controller.signal.aborted) return;
64576
64693
  if (isForeignRepo(data$2.metadata?.repoId, requestedWorkspace)) {
64577
- log$24.info("macro view graph ignored for another workspace", {
64694
+ log$25.info("macro view graph ignored for another workspace", {
64578
64695
  repoId: data$2.metadata?.repoId,
64579
64696
  requestedWorkspace,
64580
64697
  activeWorkspace: currentWorkspace
@@ -64582,7 +64699,7 @@ function useViewMode(viewMode, currentWorkspace, depsFetchAbortRef) {
64582
64699
  return;
64583
64700
  }
64584
64701
  if (!data$2.nodes || data$2.nodes.length === 0) return;
64585
- log$24.info("macro view data loaded", {
64702
+ log$25.info("macro view data loaded", {
64586
64703
  viewMode: newMode,
64587
64704
  nodes: data$2.nodes.length,
64588
64705
  edges: data$2.edges?.length ?? 0,
@@ -66629,7 +66746,7 @@ function useWorkspaceCache(opts) {
66629
66746
 
66630
66747
  //#endregion
66631
66748
  //#region src/client/hooks/useSymbolSelectListener.ts
66632
- const log$23 = scoped("symbol-select");
66749
+ const log$24 = scoped("symbol-select");
66633
66750
  function useSymbolSelectListener(opts) {
66634
66751
  useEffect(() => {
66635
66752
  const symbolHandler = (e) => {
@@ -66641,7 +66758,7 @@ function useSymbolSelectListener(opts) {
66641
66758
  if (!match$1) return;
66642
66759
  store.setSelectedNode(NodeId(match$1.id));
66643
66760
  const revealed = opts.rendererRef.current?.revealNode(match$1.id) ?? false;
66644
- if (!revealed) log$23.warn("symbol match is not revealable in the rendered graph", {
66761
+ if (!revealed) log$24.warn("symbol match is not revealable in the rendered graph", {
66645
66762
  id: match$1.id,
66646
66763
  symbol: sym
66647
66764
  });
@@ -66737,7 +66854,7 @@ function useMiniMapSync(opts) {
66737
66854
 
66738
66855
  //#endregion
66739
66856
  //#region src/client/hooks/useAutoBootstrap.ts
66740
- const log$22 = scoped("auto-bootstrap");
66857
+ const log$23 = scoped("auto-bootstrap");
66741
66858
  function useAutoBootstrap(opts) {
66742
66859
  const hasAutoScannedRef = useRef(false);
66743
66860
  useEffect(() => {
@@ -66747,10 +66864,10 @@ function useAutoBootstrap(opts) {
66747
66864
  if (!opts.codegraphReady) return;
66748
66865
  if (!opts.currentWorkspace) return;
66749
66866
  hasAutoScannedRef.current = true;
66750
- log$22.info("auto-fill: triggering bootstrap on first open", { path: opts.currentWorkspace });
66867
+ log$23.info("auto-fill: triggering bootstrap on first open", { path: opts.currentWorkspace });
66751
66868
  useGraphStore.getState().loadBootstrap().then((ok) => {
66752
66869
  if (!ok) {
66753
- log$22.info("auto-fill: bootstrap failed, falling back to scan", { path: opts.currentWorkspace });
66870
+ log$23.info("auto-fill: bootstrap failed, falling back to scan", { path: opts.currentWorkspace });
66754
66871
  opts.handleScanWorkspace();
66755
66872
  }
66756
66873
  });
@@ -67005,7 +67122,7 @@ function useViewModePanelGuard(opts) {
67005
67122
 
67006
67123
  //#endregion
67007
67124
  //#region src/client/hooks/useSearchInteraction.ts
67008
- const log$21 = scoped("panel");
67125
+ const log$22 = scoped("panel");
67009
67126
  function useSearchInteraction(opts) {
67010
67127
  const { searchQuery, rawNodes, setSelectedNode, setSelectedNodeData, getDataRef, setShowSearch, isRemoteHit } = opts;
67011
67128
  const debouncedSearch = useDebounce(searchQuery, UI.SEARCH_DEBOUNCE_MS);
@@ -67057,7 +67174,7 @@ function useSearchInteraction(opts) {
67057
67174
  setShowSearch(false);
67058
67175
  const hitLabel = mergedResults.find((r) => r.id === id)?.label ?? id;
67059
67176
  getDataRef.current.revealRemoteNode(id, hitLabel).then((ok) => {
67060
- if (!ok) log$21.warn("remote search hit could not be revealed", { id });
67177
+ if (!ok) log$22.warn("remote search hit could not be revealed", { id });
67061
67178
  });
67062
67179
  return;
67063
67180
  }
@@ -67065,10 +67182,10 @@ function useSearchInteraction(opts) {
67065
67182
  const data$2 = getDataRef.current?.getNodeData(NodeId(id)) ?? null;
67066
67183
  setSelectedNodeData(data$2);
67067
67184
  const revealed = getDataRef.current?.revealNode(id) ?? false;
67068
- if (!revealed) log$21.warn("search result is not revealable in the rendered graph", { id });
67185
+ if (!revealed) log$22.warn("search result is not revealable in the rendered graph", { id });
67069
67186
  setShowSearch(false);
67070
67187
  } catch (err) {
67071
- log$21.error("handleSelectSearchResult failed", {
67188
+ log$22.error("handleSelectSearchResult failed", {
67072
67189
  id,
67073
67190
  err
67074
67191
  });
@@ -67090,7 +67207,7 @@ function useSearchInteraction(opts) {
67090
67207
 
67091
67208
  //#endregion
67092
67209
  //#region src/client/components/ErrorBoundary.tsx
67093
- const log$20 = scoped("boundary");
67210
+ const log$21 = scoped("boundary");
67094
67211
  function ErrorFallback({ error: error$1, onRetry }) {
67095
67212
  const t$1 = useT();
67096
67213
  return /* @__PURE__ */ jsxs("div", {
@@ -67133,7 +67250,7 @@ var GraphErrorBoundary = class extends Component {
67133
67250
  }
67134
67251
  componentDidCatch(error$1, errorInfo) {
67135
67252
  this.props.onError?.(error$1, errorInfo);
67136
- log$20.error("render error caught by boundary", {
67253
+ log$21.error("render error caught by boundary", {
67137
67254
  error: error$1,
67138
67255
  componentStack: errorInfo.componentStack
67139
67256
  });
@@ -69146,6 +69263,10 @@ const ContextMenu = memo(function ContextMenu$1(props) {
69146
69263
  const menuRef = useRef(null);
69147
69264
  const [focusedIndex, setFocusedIndex] = useState(0);
69148
69265
  const triggerRef = useRef(null);
69266
+ const isPinned = useGraphStore((s) => s.pinnedIds.has(props.nodeId));
69267
+ const pinNode = useGraphStore((s) => s.pinNode);
69268
+ const unpinNode = useGraphStore((s) => s.unpinNode);
69269
+ const [copyState, setCopyState] = useState("idle");
69149
69270
  const onCloseRef = useRef(props.onClose);
69150
69271
  onCloseRef.current = props.onClose;
69151
69272
  const handleClose = useCallback(() => {
@@ -69304,6 +69425,43 @@ const ContextMenu = memo(function ContextMenu$1(props) {
69304
69425
  handleClose();
69305
69426
  }
69306
69427
  });
69428
+ actions.push({
69429
+ id: "pinNode",
69430
+ label: t$1(isPinned ? "contextMenu.unpinNode" : "contextMenu.pinNode"),
69431
+ onClick: () => {
69432
+ if (isPinned) unpinNode(props.nodeId);
69433
+ else pinNode(props.nodeId);
69434
+ handleClose();
69435
+ }
69436
+ });
69437
+ actions.push({
69438
+ id: "copyId",
69439
+ label: copyState === "copied" ? t$1("contextMenu.copied") : t$1("contextMenu.copyId"),
69440
+ onClick: () => {
69441
+ try {
69442
+ navigator.clipboard?.writeText(props.nodeId);
69443
+ setCopyState("copied");
69444
+ window.setTimeout(() => setCopyState("idle"), 1500);
69445
+ } catch {}
69446
+ }
69447
+ });
69448
+ actions.push({
69449
+ id: "expandHere",
69450
+ label: t$1("contextMenu.expandHere"),
69451
+ onClick: () => {
69452
+ props.onExpandNode(props.nodeId);
69453
+ handleClose();
69454
+ },
69455
+ separatorAfter: true
69456
+ });
69457
+ actions.push({
69458
+ id: "collapseHere",
69459
+ label: t$1("contextMenu.collapseHere"),
69460
+ onClick: () => {
69461
+ props.onCollapseNode(props.nodeId);
69462
+ handleClose();
69463
+ }
69464
+ });
69307
69465
  const menuStyle = {
69308
69466
  position: "fixed",
69309
69467
  left: `${props.position.x}px`,
@@ -69317,6 +69475,7 @@ const ContextMenu = memo(function ContextMenu$1(props) {
69317
69475
  children: actions.map((action) => /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsxs("button", {
69318
69476
  className: "cg-context-menu-item",
69319
69477
  role: "menuitem",
69478
+ "data-action-id": action.id,
69320
69479
  disabled: action.disabled,
69321
69480
  onClick: action.onClick,
69322
69481
  children: [action.icon && /* @__PURE__ */ jsx("span", {
@@ -69331,6 +69490,87 @@ const ContextMenu = memo(function ContextMenu$1(props) {
69331
69490
  }) });
69332
69491
  });
69333
69492
 
69493
+ //#endregion
69494
+ //#region src/client/components/BatchActionBar.tsx
69495
+ const log$20 = scoped("batch-bar");
69496
+ function BatchActionBar({ onClearSelection }) {
69497
+ const t$1 = useT();
69498
+ const multiSelectedIds = useGraphStore((s) => s.multiSelectedIds);
69499
+ const batchExpandSelected = useGraphStore((s) => s.batchExpandSelected);
69500
+ const batchCollapseSelected = useGraphStore((s) => s.batchCollapseSelected);
69501
+ const exportSelectedNodes = useGraphStore((s) => s.exportSelectedNodes);
69502
+ const clearMultiSelection = useGraphStore((s) => s.clearMultiSelection);
69503
+ if (multiSelectedIds.size === 0) return null;
69504
+ const handleClear = () => {
69505
+ if (onClearSelection) onClearSelection();
69506
+ else clearMultiSelection();
69507
+ };
69508
+ const handleExport = () => {
69509
+ try {
69510
+ const nodes = exportSelectedNodes();
69511
+ const payload = {
69512
+ exportedAt: new Date().toISOString(),
69513
+ selectedCount: multiSelectedIds.size,
69514
+ exportedCount: nodes.length,
69515
+ nodes
69516
+ };
69517
+ const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
69518
+ const url = URL.createObjectURL(blob);
69519
+ const a = document.createElement("a");
69520
+ a.href = url;
69521
+ a.download = `codegraph-selection-${Date.now()}.json`;
69522
+ a.click();
69523
+ URL.revokeObjectURL(url);
69524
+ } catch (err) {
69525
+ log$20.error("export selection failed", { err });
69526
+ }
69527
+ };
69528
+ return /* @__PURE__ */ jsxs("div", {
69529
+ className: "cg-batch-bar",
69530
+ role: "toolbar",
69531
+ "aria-label": t$1("batchbar.ariaLabel"),
69532
+ children: [
69533
+ /* @__PURE__ */ jsx("span", {
69534
+ className: "cg-batch-bar-count",
69535
+ "aria-live": "polite",
69536
+ children: t$1("batchbar.count", { count: multiSelectedIds.size })
69537
+ }),
69538
+ /* @__PURE__ */ jsx("button", {
69539
+ type: "button",
69540
+ className: "cg-batch-bar-btn",
69541
+ onClick: () => {
69542
+ batchExpandSelected();
69543
+ },
69544
+ "aria-label": t$1("batchbar.expandAria"),
69545
+ children: t$1("batchbar.expand")
69546
+ }),
69547
+ /* @__PURE__ */ jsx("button", {
69548
+ type: "button",
69549
+ className: "cg-batch-bar-btn",
69550
+ onClick: () => {
69551
+ batchCollapseSelected();
69552
+ },
69553
+ "aria-label": t$1("batchbar.collapseAria"),
69554
+ children: t$1("batchbar.collapse")
69555
+ }),
69556
+ /* @__PURE__ */ jsx("button", {
69557
+ type: "button",
69558
+ className: "cg-batch-bar-btn",
69559
+ onClick: handleExport,
69560
+ "aria-label": t$1("batchbar.exportAria"),
69561
+ children: t$1("batchbar.export")
69562
+ }),
69563
+ /* @__PURE__ */ jsx("button", {
69564
+ type: "button",
69565
+ className: "cg-batch-bar-btn cg-batch-bar-btn-clear",
69566
+ onClick: handleClear,
69567
+ "aria-label": t$1("batchbar.clearAria"),
69568
+ children: t$1("batchbar.clear")
69569
+ })
69570
+ ]
69571
+ });
69572
+ }
69573
+
69334
69574
  //#endregion
69335
69575
  //#region src/client/components/LoadingOverlay.tsx
69336
69576
  const LoadingOverlay = memo(function LoadingOverlay$1({ visible }) {
@@ -69728,7 +69968,7 @@ const StatusBar = memo(function StatusBar$1({ error: error$1, isLoading, lastUpd
69728
69968
  const statusText = error$1 ? t$1("state.error") : isLoading ? t$1("state.loading") : t$1("state.ready");
69729
69969
  const wsName = workspaceName ? deriveWorkspaceName(workspaceName) : "";
69730
69970
  const hasDegradations = performanceDegradations && performanceDegradations.length > 0;
69731
- const degradationLabels = hasDegradations ? performanceDegradations.map((key) => DEGRADATION_KEYS[key] ? t$1(DEGRADATION_KEYS[key]) : key).join(", ") : "";
69971
+ const degradationLabels = hasDegradations && performanceDegradations ? performanceDegradations.map((key) => DEGRADATION_KEYS[key] ? t$1(DEGRADATION_KEYS[key]) : key).join(", ") : "";
69732
69972
  const degradedRules = xuanjiSignal?.degradedRules ?? [];
69733
69973
  const violatedRules = xuanjiSignal?.invariantViolations ?? [];
69734
69974
  const showXuanji = xuanjiSignal?.applied === true;
@@ -77174,8 +77414,8 @@ function InteractiveMiniMap({ thumbnail, viewportInfo, onFocusArea, nodePosition
77174
77414
  whiteSpace: "nowrap",
77175
77415
  background: "var(--cg-bg-elevated)",
77176
77416
  color: "var(--cg-text)",
77177
- border: "1px solid var(--cg-border-strong))",
77178
- boxShadow: "var(--cg-shadow-sm))",
77417
+ border: "1px solid var(--cg-border-strong)",
77418
+ boxShadow: "var(--cg-shadow-sm)",
77179
77419
  pointerEvents: "none"
77180
77420
  },
77181
77421
  children: t$1("canvasControls.minimapFocusHint")
@@ -77206,8 +77446,8 @@ function KeyboardHintOverlay({ onClose }) {
77206
77446
  padding: "10px 12px",
77207
77447
  borderRadius: "var(--cg-radius-sm)",
77208
77448
  background: "var(--cg-surface)",
77209
- border: "1px solid var(--cg-border-strong))",
77210
- boxShadow: "var(--cg-shadow))",
77449
+ border: "1px solid var(--cg-border-strong)",
77450
+ boxShadow: "var(--cg-shadow)",
77211
77451
  pointerEvents: "auto"
77212
77452
  },
77213
77453
  children: [/* @__PURE__ */ jsxs("div", {
@@ -77721,7 +77961,9 @@ function GraphPanelInner({ className = "", sessionId, useSessions }) {
77721
77961
  onNodeContextTap: (nodeId, pos) => setContextMenu({
77722
77962
  nodeId,
77723
77963
  position: pos
77724
- })
77964
+ }),
77965
+ onSelectionToggle: (_nodeId, _selected, selectedIds) => useGraphStore.getState().syncMultiSelection(selectedIds),
77966
+ onSelectionClear: () => useGraphStore.getState().clearMultiSelection()
77725
77967
  },
77726
77968
  showCallChainRef
77727
77969
  });
@@ -78334,6 +78576,7 @@ function GraphPanelInner({ className = "", sessionId, useSessions }) {
78334
78576
  }
78335
78577
  }),
78336
78578
  tooltip && /* @__PURE__ */ jsx(GraphTooltip, { data: tooltip }),
78579
+ /* @__PURE__ */ jsx(BatchActionBar, { onClearSelection: () => renderer$1.rendererRef.current?.clearMultiSelect() }),
78337
78580
  /* @__PURE__ */ jsx(KeyboardHelp, {
78338
78581
  visible: showHelp,
78339
78582
  onClose: () => setShowHelp(false)
@@ -78364,7 +78607,7 @@ function GraphPanel(props) {
78364
78607
 
78365
78608
  //#endregion
78366
78609
  //#region src/generated/version.ts
78367
- const PLUGIN_VERSION = "0.3.0";
78610
+ const PLUGIN_VERSION = "0.3.1";
78368
78611
 
78369
78612
  //#endregion
78370
78613
  //#region src/client/utils/safeCheck.ts