@theia/ai-ide 1.72.1 → 1.72.3

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.
Files changed (35) hide show
  1. package/lib/browser/ai-configuration/ai-configuration-view-contribution.d.ts +1 -0
  2. package/lib/browser/ai-configuration/ai-configuration-view-contribution.d.ts.map +1 -1
  3. package/lib/browser/ai-configuration/ai-configuration-view-contribution.js +15 -2
  4. package/lib/browser/ai-configuration/ai-configuration-view-contribution.js.map +1 -1
  5. package/lib/browser/ai-configuration/tools-configuration-widget.d.ts +3 -0
  6. package/lib/browser/ai-configuration/tools-configuration-widget.d.ts.map +1 -1
  7. package/lib/browser/ai-configuration/tools-configuration-widget.js +61 -4
  8. package/lib/browser/ai-configuration/tools-configuration-widget.js.map +1 -1
  9. package/lib/browser/ide-chat-welcome-message-provider.d.ts +11 -0
  10. package/lib/browser/ide-chat-welcome-message-provider.d.ts.map +1 -1
  11. package/lib/browser/ide-chat-welcome-message-provider.js +31 -1
  12. package/lib/browser/ide-chat-welcome-message-provider.js.map +1 -1
  13. package/lib/browser/todo-tool-renderer.d.ts +7 -1
  14. package/lib/browser/todo-tool-renderer.d.ts.map +1 -1
  15. package/lib/browser/todo-tool-renderer.js +25 -1
  16. package/lib/browser/todo-tool-renderer.js.map +1 -1
  17. package/lib/browser/user-interaction-tool-renderer.d.ts +7 -1
  18. package/lib/browser/user-interaction-tool-renderer.d.ts.map +1 -1
  19. package/lib/browser/user-interaction-tool-renderer.js +25 -1
  20. package/lib/browser/user-interaction-tool-renderer.js.map +1 -1
  21. package/lib/browser/workspace-functions.d.ts +27 -3
  22. package/lib/browser/workspace-functions.d.ts.map +1 -1
  23. package/lib/browser/workspace-functions.js +123 -140
  24. package/lib/browser/workspace-functions.js.map +1 -1
  25. package/lib/browser/workspace-functions.spec.js +323 -24
  26. package/lib/browser/workspace-functions.spec.js.map +1 -1
  27. package/package.json +23 -22
  28. package/src/browser/ai-configuration/ai-configuration-view-contribution.ts +15 -1
  29. package/src/browser/ai-configuration/tools-configuration-widget.tsx +106 -17
  30. package/src/browser/ide-chat-welcome-message-provider.tsx +43 -1
  31. package/src/browser/style/index.css +16 -0
  32. package/src/browser/todo-tool-renderer.tsx +30 -3
  33. package/src/browser/user-interaction-tool-renderer.tsx +30 -3
  34. package/src/browser/workspace-functions.spec.ts +385 -25
  35. package/src/browser/workspace-functions.ts +128 -195
@@ -20,6 +20,7 @@ import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
20
20
  import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
21
21
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
22
22
  import { FileStat, FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files';
23
+ import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
23
24
  import { WorkspaceService } from '@theia/workspace/lib/browser';
24
25
  import {
25
26
  FILE_CONTENT_FUNCTION_ID, GET_FILE_DIAGNOSTICS_ID,
@@ -59,7 +60,7 @@ export class WorkspaceFunctionScope {
59
60
  @inject(EnvVariablesServer)
60
61
  protected readonly envVariablesServer: EnvVariablesServer;
61
62
 
62
- private gitignoreMatchers = new Map<string, ReturnType<typeof ignore>>();
63
+ private gitignoreMatchers = new Map<string, ReturnType<typeof ignore> | undefined>();
63
64
  private gitignoreWatchersInitialized = new Set<string>();
64
65
 
65
66
  private _rootMapping: Map<string, URI> | undefined;
@@ -398,7 +399,9 @@ export class WorkspaceFunctionScope {
398
399
  }
399
400
  const uri = await this.toExternalUri(trimmed);
400
401
  if (uri && uri.scheme === 'file') {
401
- result.push(uri.normalizePath());
402
+ // Strip a trailing separator so an entry like `/foo/` still matches the
403
+ // directory `/foo` itself (URI.isEqualOrParent compares the last segment exactly).
404
+ result.push(WorkspaceFunctionScope.withoutTrailingSeparator(uri.normalizePath()));
402
405
  }
403
406
  }
404
407
  return result;
@@ -473,6 +476,20 @@ export class WorkspaceFunctionScope {
473
476
  return /^[A-Za-z]:\//.test(normalized);
474
477
  }
475
478
 
479
+ /**
480
+ * Returns the URI without a trailing path separator (except for a root path). This makes
481
+ * directory comparisons via {@link URI.isEqualOrParent} insensitive to a trailing slash, so
482
+ * an allow-list entry such as `/foo/` matches the directory `/foo` itself, not only its
483
+ * children.
484
+ */
485
+ static withoutTrailingSeparator(uri: URI): URI {
486
+ const path = uri.path.toString();
487
+ if (path.length > 1 && path.endsWith('/')) {
488
+ return uri.withPath(path.substring(0, path.length - 1));
489
+ }
490
+ return uri;
491
+ }
492
+
476
493
  protected getHomeDirUri(): Promise<URI | undefined> {
477
494
  if (!this.homeDirUri) {
478
495
  this.homeDirUri = this.envVariablesServer.getHomeDirUri()
@@ -533,33 +550,43 @@ export class WorkspaceFunctionScope {
533
550
  }
534
551
 
535
552
  protected async isGitIgnored(stat: FileStat, workspaceRoot: URI): Promise<boolean> {
553
+ const matcher = await this.getGitignoreMatcher(workspaceRoot);
554
+ if (!matcher) {
555
+ return false;
556
+ }
557
+ const relativePath = workspaceRoot.relative(stat.resource);
558
+ if (!relativePath) {
559
+ return false;
560
+ }
561
+ const relativePathStr = relativePath.toString() + (stat.isDirectory ? '/' : '');
562
+ return matcher.ignores(relativePathStr);
563
+ }
564
+
565
+ /**
566
+ * Returns the cached `.gitignore` matcher for the given root, reading the file at most
567
+ * once per root. A root with no (or unreadable) `.gitignore` is cached as `undefined`.
568
+ * The cache is invalidated by {@link initializeGitignoreWatcher} when the `.gitignore` is
569
+ * created, changed, or deleted, so individual exclusion checks need no filesystem RPC.
570
+ */
571
+ protected async getGitignoreMatcher(workspaceRoot: URI): Promise<ReturnType<typeof ignore> | undefined> {
536
572
  await this.initializeGitignoreWatcher(workspaceRoot);
537
573
 
538
574
  const rootKey = workspaceRoot.toString();
539
- const gitignoreUri = workspaceRoot.resolve(this.GITIGNORE_FILE_NAME);
575
+ if (this.gitignoreMatchers.has(rootKey)) {
576
+ return this.gitignoreMatchers.get(rootKey);
577
+ }
540
578
 
579
+ let matcher: ReturnType<typeof ignore> | undefined;
541
580
  try {
542
- const fileStat = await this.fileService.resolve(gitignoreUri);
543
- if (fileStat) {
544
- let matcher = this.gitignoreMatchers.get(rootKey);
545
- if (!matcher) {
546
- const gitignoreContent = await this.fileService.read(gitignoreUri);
547
- matcher = ignore().add(gitignoreContent.value);
548
- this.gitignoreMatchers.set(rootKey, matcher);
549
- }
550
- const relativePath = workspaceRoot.relative(stat.resource);
551
- if (relativePath) {
552
- const relativePathStr = relativePath.toString() + (stat.isDirectory ? '/' : '');
553
- if (matcher.ignores(relativePathStr)) {
554
- return true;
555
- }
556
- }
557
- }
581
+ const gitignoreUri = workspaceRoot.resolve(this.GITIGNORE_FILE_NAME);
582
+ const gitignoreContent = await this.fileService.read(gitignoreUri);
583
+ matcher = ignore().add(gitignoreContent.value);
558
584
  } catch {
559
- // If .gitignore does not exist or cannot be read, continue without error
585
+ // No .gitignore (or it cannot be read): cache the absence so we don't retry on every check.
586
+ matcher = undefined;
560
587
  }
561
-
562
- return false;
588
+ this.gitignoreMatchers.set(rootKey, matcher);
589
+ return matcher;
563
590
  }
564
591
  }
565
592
 
@@ -653,17 +680,25 @@ export class GetWorkspaceDirectoryStructure implements ToolProvider {
653
680
  const result: Record<string, unknown> = {};
654
681
 
655
682
  if (stat && stat.isDirectory && stat.children) {
683
+ // Determine which child directories to include (the exclusion check may be async)...
684
+ const childDirs: URI[] = [];
656
685
  for (const child of stat.children) {
657
686
  if (cancellationToken?.isCancellationRequested) {
658
687
  return { error: 'Operation cancelled by user' };
659
688
  }
660
-
661
- if (!child.isDirectory || (await this.workspaceScope.shouldExclude(child))) {
662
- continue;
689
+ if (child.isDirectory && !(await this.workspaceScope.shouldExclude(child))) {
690
+ childDirs.push(child.resource);
663
691
  }
664
- const dirName = child.resource.path.base;
665
- result[dirName] = await this.buildDirectoryStructure(child.resource, cancellationToken);
666
692
  }
693
+ // ...then resolve their subtrees concurrently, so the traversal costs O(depth)
694
+ // round-trips instead of one serial round-trip per directory. Empty directories
695
+ // are preserved (they resolve to an empty object).
696
+ const subtrees = await Promise.all(
697
+ childDirs.map(childUri => this.buildDirectoryStructure(childUri, cancellationToken))
698
+ );
699
+ childDirs.forEach((childUri, index) => {
700
+ result[childUri.path.base] = subtrees[index];
701
+ });
667
702
  }
668
703
 
669
704
  return result;
@@ -1028,37 +1063,32 @@ export class GetWorkspaceFileList implements ToolProvider {
1028
1063
  if (!stat || !stat.isDirectory) {
1029
1064
  return JSON.stringify({ error: 'Directory not found' });
1030
1065
  }
1031
- return await this.listFilesDirectly(targetUri, cancellationToken);
1066
+ return await this.listFilesDirectly(stat, cancellationToken);
1032
1067
  } catch (error) {
1033
1068
  return JSON.stringify({ error: 'Directory not found' });
1034
1069
  }
1035
1070
  }
1036
1071
 
1037
- private async listFilesDirectly(uri: URI, cancellationToken?: CancellationToken): Promise<string> {
1072
+ private async listFilesDirectly(stat: FileStat, cancellationToken?: CancellationToken): Promise<string> {
1038
1073
  if (cancellationToken?.isCancellationRequested) {
1039
1074
  return JSON.stringify({ error: 'Operation cancelled by user' });
1040
1075
  }
1041
1076
 
1042
- const stat = await this.fileService.resolve(uri);
1043
1077
  const result: Record<string, 'directory' | 'file'> = {};
1044
1078
 
1045
- if (stat && stat.isDirectory) {
1046
- if (await this.workspaceScope.shouldExclude(stat)) {
1047
- return JSON.stringify(result);
1079
+ if (await this.workspaceScope.shouldExclude(stat)) {
1080
+ return JSON.stringify(result);
1081
+ }
1082
+ // `stat` already carries one level of children from the caller's resolve, so no extra RPC.
1083
+ for (const child of stat.children ?? []) {
1084
+ if (cancellationToken?.isCancellationRequested) {
1085
+ return JSON.stringify({ error: 'Operation cancelled by user' });
1048
1086
  }
1049
- const children = await this.fileService.resolve(uri);
1050
- if (children.children) {
1051
- for (const child of children.children) {
1052
- if (cancellationToken?.isCancellationRequested) {
1053
- return JSON.stringify({ error: 'Operation cancelled by user' });
1054
- }
1055
1087
 
1056
- if (await this.workspaceScope.shouldExclude(child)) {
1057
- continue;
1058
- }
1059
- result[child.resource.path.base] = child.isDirectory ? 'directory' : 'file';
1060
- }
1088
+ if (await this.workspaceScope.shouldExclude(child)) {
1089
+ continue;
1061
1090
  }
1091
+ result[child.resource.path.base] = child.isDirectory ? 'directory' : 'file';
1062
1092
  }
1063
1093
 
1064
1094
  return JSON.stringify(result);
@@ -1224,8 +1254,8 @@ export class FindFilesByPattern implements ToolProvider {
1224
1254
  @inject(PreferenceService)
1225
1255
  protected readonly preferences: PreferenceService;
1226
1256
 
1227
- @inject(FileService)
1228
- protected readonly fileService: FileService;
1257
+ @inject(FileSearchService)
1258
+ protected readonly fileSearchService: FileSearchService;
1229
1259
 
1230
1260
  getTool(): ToolRequest {
1231
1261
  return {
@@ -1239,8 +1269,6 @@ export class FindFilesByPattern implements ToolProvider {
1239
1269
  '\'src/**/*.js\' for JavaScript files in the src directory. The function respects gitignore patterns and user exclusions, ' +
1240
1270
  'returns workspace-relative paths (e.g., "my-project/src/index.ts") or absolute paths for external roots, ' +
1241
1271
  'and limits results to 200 files maximum. ' +
1242
- 'Performance note: This traverses directories recursively which may be slow in large workspaces. ' +
1243
- 'For better performance, use specific subdirectory patterns (e.g., \'src/**/*.ts\' instead of \'**/*.ts\'). ' +
1244
1272
  'Use this to find files by name/extension. Do NOT use this for searching file contents - use searchInWorkspace instead.',
1245
1273
  parameters: {
1246
1274
  type: 'object',
@@ -1249,8 +1277,7 @@ export class FindFilesByPattern implements ToolProvider {
1249
1277
  type: 'string',
1250
1278
  description: 'Glob pattern to match files against. ' +
1251
1279
  'Examples: \'**/*.ts\' (all TypeScript files), \'src/**/*.js\' (JS files in src), ' +
1252
- '\'**/*.{js,ts}\' (JS or TS files), \'**/test/**/*.spec.ts\' (test files). ' +
1253
- 'Use specific subdirectory prefixes for better performance (e.g., \'packages/core/**/*.ts\' instead of \'**/*.ts\').'
1280
+ '\'**/*.{js,ts}\' (JS or TS files), \'**/test/**/*.spec.ts\' (test files).'
1254
1281
  },
1255
1282
  exclude: {
1256
1283
  type: 'array',
@@ -1303,76 +1330,59 @@ export class FindFilesByPattern implements ToolProvider {
1303
1330
  }
1304
1331
 
1305
1332
  try {
1306
- const patternMatcher = new Minimatch(pattern, { dot: false });
1307
- const files: string[] = [];
1308
1333
  const maxResults = 200;
1334
+ const useGitIgnore = this.preferences.get(CONSIDER_GITIGNORE_PREF, true);
1335
+ const userExcludes = this.preferences.get<string[]>(USER_EXCLUDE_PATTERN_PREF, []);
1336
+ const excludes = [...userExcludes, ...(excludePatterns ?? [])];
1309
1337
 
1338
+ // Resolve the set of roots to search and how each root's results should be rendered.
1339
+ const targets: { rootUri: URI; rootName?: string; external: boolean }[] = [];
1310
1340
  if (searchRoot) {
1311
1341
  const resolved = await this.workspaceScope.resolveToUri(searchRoot);
1312
1342
  if (!resolved) {
1313
1343
  return JSON.stringify({ error: `Invalid searchRoot: '${searchRoot}'` });
1314
1344
  }
1315
- const rootUri = resolved;
1316
- const isExternalRoot = !this.workspaceScope.isInWorkspace(rootUri);
1317
- await this.workspaceScope.ensureAccessible(rootUri);
1318
-
1319
- const ignorePatterns = isExternalRoot
1320
- ? this.preferences.get<string[]>(USER_EXCLUDE_PATTERN_PREF, [])
1321
- : await this.buildIgnorePatterns(rootUri);
1322
- const allExcludes = [...ignorePatterns];
1323
- if (excludePatterns && excludePatterns.length > 0) {
1324
- allExcludes.push(...excludePatterns);
1325
- }
1326
-
1327
- if (cancellationToken?.isCancellationRequested) {
1328
- return JSON.stringify({ error: 'Operation cancelled by user' });
1329
- }
1330
-
1331
- const excludeMatchers = allExcludes.map(excludePattern => new Minimatch(excludePattern, { dot: true }));
1332
-
1333
- await this.traverseDirectory(
1334
- rootUri,
1335
- rootUri,
1336
- undefined,
1337
- patternMatcher,
1338
- excludeMatchers,
1339
- files,
1340
- maxResults,
1341
- cancellationToken,
1342
- isExternalRoot
1343
- );
1345
+ await this.workspaceScope.ensureAccessible(resolved);
1346
+ targets.push({ rootUri: resolved, external: !this.workspaceScope.isInWorkspace(resolved) });
1344
1347
  } else {
1345
1348
  const rootMapping = this.workspaceScope.getRootMapping();
1346
1349
  if (rootMapping.size === 0) {
1347
1350
  return JSON.stringify({ error: 'No workspace has been opened yet' });
1348
1351
  }
1349
-
1350
1352
  for (const [rootName, rootUri] of rootMapping) {
1351
- if (cancellationToken?.isCancellationRequested) {
1352
- return JSON.stringify({ error: 'Operation cancelled by user' });
1353
- }
1354
-
1355
- if (files.length >= maxResults) {
1356
- break;
1357
- }
1353
+ targets.push({ rootUri, rootName, external: false });
1354
+ }
1355
+ }
1358
1356
 
1359
- const ignorePatterns = await this.buildIgnorePatterns(rootUri);
1360
- const allExcludes = [...ignorePatterns];
1361
- if (excludePatterns && excludePatterns.length > 0) {
1362
- allExcludes.push(...excludePatterns);
1357
+ // Delegate the actual traversal to the backend ripgrep-based file search.
1358
+ // It runs natively on the backend filesystem (no per-directory RPC) and applies
1359
+ // include/exclude globs.
1360
+ const files: string[] = [];
1361
+ for (const target of targets) {
1362
+ if (cancellationToken?.isCancellationRequested) {
1363
+ return JSON.stringify({ error: 'Operation cancelled by user' });
1364
+ }
1365
+ if (files.length > maxResults) {
1366
+ break;
1367
+ }
1368
+ // `considerGitIgnore` is scoped to workspace roots (see its preference description),
1369
+ // so external allow-listed roots are searched with user/caller excludes only (plus
1370
+ // `.git`). Applying gitignore there would also leak the user's *global* gitignore
1371
+ // into an explicitly allow-listed directory and silently hide files.
1372
+ // Request one extra result across all roots so we can detect truncation.
1373
+ const matches = await this.fileSearchService.find('', {
1374
+ rootUris: [target.rootUri.toString()],
1375
+ includePatterns: [pattern],
1376
+ excludePatterns: target.external ? [...excludes, '.git'] : excludes,
1377
+ useGitIgnore: target.external ? false : useGitIgnore,
1378
+ fuzzyMatch: false,
1379
+ limit: maxResults - files.length + 1
1380
+ }, cancellationToken);
1381
+ for (const match of matches) {
1382
+ const display = this.toDisplayPath(new URI(match), target);
1383
+ if (display !== undefined) {
1384
+ files.push(display);
1363
1385
  }
1364
- const excludeMatchers = allExcludes.map(excludePattern => new Minimatch(excludePattern, { dot: true }));
1365
-
1366
- await this.traverseDirectory(
1367
- rootUri,
1368
- rootUri,
1369
- rootName,
1370
- patternMatcher,
1371
- excludeMatchers,
1372
- files,
1373
- maxResults,
1374
- cancellationToken
1375
- );
1376
1386
  }
1377
1387
  }
1378
1388
 
@@ -1380,12 +1390,10 @@ export class FindFilesByPattern implements ToolProvider {
1380
1390
  return JSON.stringify({ error: 'Operation cancelled by user' });
1381
1391
  }
1382
1392
 
1383
- const result: { files: string[]; totalFound?: number; truncated?: boolean } = {
1393
+ const result: { files: string[]; truncated?: boolean } = {
1384
1394
  files: files.slice(0, maxResults)
1385
1395
  };
1386
-
1387
1396
  if (files.length > maxResults) {
1388
- result.totalFound = files.length;
1389
1397
  result.truncated = true;
1390
1398
  }
1391
1399
 
@@ -1396,94 +1404,19 @@ export class FindFilesByPattern implements ToolProvider {
1396
1404
  }
1397
1405
  }
1398
1406
 
1399
- private async buildIgnorePatterns(workspaceRoot: URI): Promise<string[]> {
1400
- const patterns: string[] = [];
1401
-
1402
- // Get user exclude patterns from preferences
1403
- const userExcludePatterns = this.preferences.get<string[]>(USER_EXCLUDE_PATTERN_PREF, []);
1404
- patterns.push(...userExcludePatterns);
1405
-
1406
- // Add gitignore patterns if enabled
1407
- const shouldConsiderGitIgnore = this.preferences.get(CONSIDER_GITIGNORE_PREF, false);
1408
- if (shouldConsiderGitIgnore) {
1409
- try {
1410
- const gitignoreUri = workspaceRoot.resolve('.gitignore');
1411
- const gitignoreContent = await this.fileService.read(gitignoreUri);
1412
- const gitignoreLines = gitignoreContent.value
1413
- .split('\n')
1414
- .map(line => line.trim())
1415
- .filter(line => line && !line.startsWith('#'));
1416
- patterns.push(...gitignoreLines);
1417
- } catch {
1418
- // Gitignore file doesn't exist or can't be read, continue without it
1419
- }
1420
- }
1421
-
1422
- return patterns;
1423
- }
1424
-
1425
- private async traverseDirectory(
1426
- currentUri: URI,
1427
- searchRoot: URI,
1428
- rootName: string | undefined,
1429
- patternMatcher: Minimatch,
1430
- excludeMatchers: Minimatch[],
1431
- results: string[],
1432
- maxResults: number,
1433
- cancellationToken?: CancellationToken,
1434
- emitAbsolutePaths = false
1435
- ): Promise<void> {
1436
- if (cancellationToken?.isCancellationRequested || results.length >= maxResults) {
1437
- return;
1407
+ /**
1408
+ * Renders a search-result URI in the format expected by the caller: an absolute
1409
+ * path for external roots, or a `<rootName>/<relativePath>` (or bare relative
1410
+ * path when no root name is available) for workspace roots.
1411
+ */
1412
+ protected toDisplayPath(match: URI, target: { rootUri: URI; rootName?: string; external: boolean }): string | undefined {
1413
+ if (target.external) {
1414
+ return match.path.toString();
1438
1415
  }
1439
-
1440
- try {
1441
- const stat = await this.fileService.resolve(currentUri);
1442
- if (!stat || !stat.isDirectory || !stat.children) {
1443
- return;
1444
- }
1445
-
1446
- for (const child of stat.children) {
1447
- if (cancellationToken?.isCancellationRequested || results.length >= maxResults) {
1448
- break;
1449
- }
1450
-
1451
- const relativePath = searchRoot.relative(child.resource)?.toString();
1452
- if (!relativePath) {
1453
- continue;
1454
- }
1455
-
1456
- const shouldExclude = excludeMatchers.some(matcher => matcher.match(relativePath)) ||
1457
- (await this.workspaceScope.shouldExclude(child));
1458
-
1459
- if (shouldExclude) {
1460
- continue;
1461
- }
1462
-
1463
- if (child.isDirectory) {
1464
- await this.traverseDirectory(
1465
- child.resource,
1466
- searchRoot,
1467
- rootName,
1468
- patternMatcher,
1469
- excludeMatchers,
1470
- results,
1471
- maxResults,
1472
- cancellationToken,
1473
- emitAbsolutePaths
1474
- );
1475
- } else if (patternMatcher.match(relativePath)) {
1476
- if (emitAbsolutePaths) {
1477
- results.push(child.resource.path.toString());
1478
- } else if (rootName) {
1479
- results.push(`${rootName}/${relativePath}`);
1480
- } else {
1481
- results.push(relativePath);
1482
- }
1483
- }
1484
- }
1485
- } catch {
1486
- // If we can't access a directory, skip it
1416
+ const relativePath = target.rootUri.relative(match)?.toString();
1417
+ if (relativePath === undefined) {
1418
+ return undefined;
1487
1419
  }
1420
+ return target.rootName ? `${target.rootName}/${relativePath}` : relativePath;
1488
1421
  }
1489
1422
  }