chrome-devtools-frontend 1.0.1701636 → 1.0.1703574

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 (91) hide show
  1. package/eslint.config.mjs +3 -1
  2. package/front_end/core/common/ParsedURL.ts +5 -1
  3. package/front_end/core/host/UserMetricsEnums.ts +2 -1
  4. package/front_end/core/root/ExperimentNames.ts +1 -0
  5. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +2 -7
  6. package/front_end/core/text_utils/Markdown.ts +73 -0
  7. package/front_end/core/text_utils/text_utils.ts +2 -0
  8. package/front_end/devtools_compatibility.js +2 -1
  9. package/front_end/entrypoints/main/MainImpl.ts +8 -0
  10. package/front_end/generated/InspectorBackendCommands.ts +6 -4
  11. package/front_end/generated/protocol-mapping.d.ts +8 -0
  12. package/front_end/generated/protocol-proxy-api.d.ts +6 -0
  13. package/front_end/generated/protocol.ts +22 -3
  14. package/front_end/models/ai_assistance/AiAgent2.ts +13 -1
  15. package/front_end/models/ai_assistance/AiConversation.ts +1 -1
  16. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +13 -19
  17. package/front_end/models/change_tracker/ChangeTracker.ts +5 -64
  18. package/front_end/models/comment_manager/CD4ABridge.ts +5 -0
  19. package/front_end/models/comment_manager/CommentManager.ts +3 -5
  20. package/front_end/models/comment_manager/CommentThread.ts +30 -13
  21. package/front_end/models/issues_manager/CookieDeprecationMetadataIssue.ts +6 -7
  22. package/front_end/models/issues_manager/DeprecationIssue.ts +1 -1
  23. package/front_end/models/issues_manager/MarkdownIssueDescription.ts +18 -52
  24. package/front_end/models/issues_manager/SRIMessageSignatureIssue.ts +2 -4
  25. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantRead.md +0 -1
  26. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantReadOptOut.md +5 -0
  27. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantSet.md +0 -1
  28. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantSetOptOut.md +5 -0
  29. package/front_end/models/issues_manager/descriptions/deprecation.md +1 -1
  30. package/front_end/models/issues_manager/descriptions/sriValidationFailedIntegrityMismatch.md +2 -0
  31. package/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts +1 -1
  32. package/front_end/models/persistence/NetworkPersistenceManager.ts +14 -2
  33. package/front_end/panels/common/CommentThreadWidget.ts +29 -10
  34. package/front_end/panels/common/CommentsOverlayWidget.ts +50 -14
  35. package/front_end/panels/common/CommentsStatusBarPill.ts +40 -7
  36. package/front_end/panels/common/commentThreadWidget.css +11 -0
  37. package/front_end/panels/common/commentsStatusBarPill.css +13 -0
  38. package/front_end/panels/elements/StylePropertiesSection.ts +10 -0
  39. package/front_end/panels/elements/StylePropertyTreeElement.ts +1 -1
  40. package/front_end/panels/elements/StylesSidebarPane.ts +101 -49
  41. package/front_end/panels/elements/components/CSSQuery.ts +1 -1
  42. package/front_end/panels/elements/components/cssQuery.css +4 -0
  43. package/front_end/panels/elements/elements.ts +0 -1
  44. package/front_end/panels/elements/stylesSidebarPane.css +6 -0
  45. package/front_end/panels/issues/IssueView.ts +5 -1
  46. package/front_end/panels/sources/ResourceOriginPlugin.ts +8 -3
  47. package/front_end/panels/sources/ScopeChainSidebarPane.ts +3 -4
  48. package/front_end/panels/sources/SourcesView.ts +127 -147
  49. package/front_end/panels/sources/TabbedEditorContainer.ts +518 -246
  50. package/front_end/panels/sources/UISourceCodeFrame.ts +9 -0
  51. package/front_end/third_party/chromium/README.chromium +1 -1
  52. package/front_end/third_party/chromium/client-variations/ClientVariations.js +25 -96
  53. package/front_end/third_party/codemirror.next/chunk/angular.js +1 -1
  54. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  55. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  56. package/front_end/third_party/codemirror.next/chunk/cpp.js +1 -1
  57. package/front_end/third_party/codemirror.next/chunk/cpp.js.map +1 -1
  58. package/front_end/third_party/codemirror.next/chunk/java.js +1 -1
  59. package/front_end/third_party/codemirror.next/chunk/java.js.map +1 -1
  60. package/front_end/third_party/codemirror.next/chunk/legacy.js +1 -1
  61. package/front_end/third_party/codemirror.next/chunk/less.js +1 -1
  62. package/front_end/third_party/codemirror.next/chunk/less.js.map +1 -1
  63. package/front_end/third_party/codemirror.next/chunk/markdown.js +1 -1
  64. package/front_end/third_party/codemirror.next/chunk/markdown.js.map +1 -1
  65. package/front_end/third_party/codemirror.next/chunk/php.js +1 -1
  66. package/front_end/third_party/codemirror.next/chunk/php.js.map +1 -1
  67. package/front_end/third_party/codemirror.next/chunk/python.js +1 -1
  68. package/front_end/third_party/codemirror.next/chunk/python.js.map +1 -1
  69. package/front_end/third_party/codemirror.next/chunk/sass.js +1 -1
  70. package/front_end/third_party/codemirror.next/chunk/sass.js.map +1 -1
  71. package/front_end/third_party/codemirror.next/chunk/svelte.js +1 -1
  72. package/front_end/third_party/codemirror.next/chunk/svelte.js.map +1 -1
  73. package/front_end/third_party/codemirror.next/chunk/vue.js +1 -1
  74. package/front_end/third_party/codemirror.next/chunk/wast.js +1 -1
  75. package/front_end/third_party/codemirror.next/chunk/wast.js.map +1 -1
  76. package/front_end/third_party/codemirror.next/chunk/xml.js +1 -1
  77. package/front_end/third_party/codemirror.next/chunk/xml.js.map +1 -1
  78. package/front_end/third_party/codemirror.next/codemirror.next.js +1 -1
  79. package/front_end/third_party/codemirror.next/package.json +4 -1
  80. package/front_end/third_party/codemirror.next/rebuild.sh +1 -1
  81. package/front_end/ui/comments/CommentOverlayManager.ts +11 -10
  82. package/front_end/ui/components/markdown_view/MarkdownPlaceholderLitRenderer.ts +59 -0
  83. package/front_end/ui/components/markdown_view/markdown_view.ts +2 -0
  84. package/front_end/ui/legacy/TabbedPane.ts +51 -40
  85. package/front_end/ui/legacy/Treeoutline.ts +28 -19
  86. package/front_end/ui/legacy/UIUtils.ts +1 -1
  87. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +424 -570
  88. package/front_end/ui/legacy/components/object_ui/objectPropertiesSection.css +2 -27
  89. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  90. package/mcp/mcp.ts +1 -0
  91. package/package.json +2 -1
@@ -58,13 +58,7 @@ export interface Comment {
58
58
  timestamp: number;
59
59
  }
60
60
 
61
- export interface ChangeRecord {
62
- id: string;
63
- description: string;
64
- timestamp: number;
65
- }
66
-
67
- export type CommentThreadStatus = 'DRAFT'|'ACTIVE'|'RESOLVED';
61
+ export type CommentThreadStatus = 'DRAFT'|'ACTIVE'|'SENT_TO_AGENT'|'RESOLVED';
68
62
 
69
63
  export const enum Events {
70
64
  CHANGED = 'Changed',
@@ -77,7 +71,8 @@ export interface EventTypes {
77
71
  export interface CommentThreadOptions {
78
72
  anchor: CommentAnchorSignature;
79
73
  comments?: Comment[];
80
- changes?: ChangeRecord[];
74
+ /** True for comments generated from the change tracker. */
75
+ isGeneratedComment?: boolean;
81
76
  }
82
77
 
83
78
  export class CommentThread extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
@@ -89,33 +84,41 @@ export class CommentThread extends Common.ObjectWrapper.ObjectWrapper<EventTypes
89
84
 
90
85
  readonly id: string = crypto.randomUUID();
91
86
  readonly anchor: CommentAnchorSignature;
87
+ /** True for comments generated from the change tracker. */
88
+ readonly isGeneratedComment: boolean;
92
89
  #savedIndex?: number;
93
90
  comments: Comment[];
94
91
  status: CommentThreadStatus = 'DRAFT';
95
92
  transmitted: boolean = false;
96
- changes?: ChangeRecord[];
97
93
 
98
94
  constructor(options: CommentThreadOptions) {
99
95
  super();
100
96
  this.anchor = options.anchor;
101
97
  this.comments = options.comments ?? [];
102
- this.changes = options.changes;
98
+ this.isGeneratedComment = Boolean(options.isGeneratedComment);
103
99
  }
104
100
 
105
101
  get index(): number {
106
102
  return this.#savedIndex ?? CommentThread.#nextIndex;
107
103
  }
108
104
 
109
- save(text?: string, author: 'DEVELOPER'|'AGENT' = 'DEVELOPER'): void {
110
- let changed = false;
105
+ /**
106
+ * Returns whether it was changed.
107
+ */
108
+ #saveText(text?: string, author: 'DEVELOPER'|'AGENT' = 'DEVELOPER'): boolean {
111
109
  if (text && text.trim().length > 0) {
112
110
  this.comments.push({
113
111
  author,
114
112
  text: text.trim(),
115
113
  timestamp: Date.now(),
116
114
  });
117
- changed = true;
115
+ return true;
118
116
  }
117
+ return false;
118
+ }
119
+
120
+ save(text?: string, author: 'DEVELOPER'|'AGENT' = 'DEVELOPER'): void {
121
+ let changed = this.#saveText(text, author);
119
122
  if (this.status === 'DRAFT') {
120
123
  if (this.#savedIndex === undefined) {
121
124
  this.#savedIndex = CommentThread.#nextIndex++;
@@ -128,6 +131,20 @@ export class CommentThread extends Common.ObjectWrapper.ObjectWrapper<EventTypes
128
131
  }
129
132
  }
130
133
 
134
+ sendToAgent(text?: string, author: 'DEVELOPER'|'AGENT' = 'DEVELOPER'): void {
135
+ let changed = this.#saveText(text, author);
136
+ if (this.status === 'DRAFT' || this.status === 'ACTIVE') {
137
+ if (this.#savedIndex === undefined) {
138
+ this.#savedIndex = CommentThread.#nextIndex++;
139
+ }
140
+ this.status = 'SENT_TO_AGENT';
141
+ changed = true;
142
+ }
143
+ if (changed) {
144
+ this.dispatchEventToListeners(Events.CHANGED);
145
+ }
146
+ }
147
+
131
148
  resolve(replyText?: string): void {
132
149
  if (replyText && replyText.trim().length > 0) {
133
150
  this.comments.push({
@@ -33,20 +33,19 @@ export class CookieDeprecationMetadataIssue extends Issue<Protocol.Audits.Cookie
33
33
  }
34
34
 
35
35
  getDescription(): MarkdownIssueDescription {
36
- const fileName =
36
+ let fileName =
37
37
  this.details().operation === 'SetCookie' ? 'cookieWarnMetadataGrantSet.md' : 'cookieWarnMetadataGrantRead.md';
38
38
 
39
- let optOutText = '';
39
+ const substitutions = new Map<string, string>();
40
40
  if (this.details().isOptOutTopLevel) {
41
- optOutText = '\n\n (Top level site opt-out: ' + this.details().optOutPercentage +
42
- '% - [learn more](gracePeriodStagedControlExplainer))';
41
+ fileName = this.details().operation === 'SetCookie' ? 'cookieWarnMetadataGrantSetOptOut.md' :
42
+ 'cookieWarnMetadataGrantReadOptOut.md';
43
+ substitutions.set('PLACEHOLDER_optOutPercentage', String(this.details().optOutPercentage));
43
44
  }
44
45
 
45
46
  return {
46
47
  file: fileName,
47
- substitutions: new Map([
48
- ['PLACEHOLDER_topleveloptout', optOutText],
49
- ]),
48
+ substitutions,
50
49
  links: [
51
50
  {
52
51
  link: 'https://goo.gle/changes-to-chrome-browsing',
@@ -71,8 +71,8 @@ export class DeprecationIssue extends Issue<Protocol.Audits.DeprecationIssueDeta
71
71
  }
72
72
  return resolveLazyDescription({
73
73
  file: 'deprecation.md',
74
+ title: i18nLazyString(UIStrings.title),
74
75
  substitutions: new Map([
75
- ['PLACEHOLDER_title', i18nLazyString(UIStrings.title)],
76
76
  ['PLACEHOLDER_message', messageFunction],
77
77
  ]),
78
78
  links,
@@ -3,6 +3,7 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as Platform from '../../core/platform/platform.js';
6
+ import * as TextUtils from '../../core/text_utils/text_utils.js';
6
7
  import * as Marked from '../../third_party/marked/marked.js';
7
8
 
8
9
  /**
@@ -13,12 +14,14 @@ import * as Marked from '../../third_party/marked/marked.js';
13
14
  */
14
15
  export interface MarkdownIssueDescription {
15
16
  file: string;
17
+ title?: string;
16
18
  substitutions?: Map<string, string>;
17
19
  links: Array<{link: string, linkTitle: string}>;
18
20
  }
19
21
 
20
22
  export interface LazyMarkdownIssueDescription {
21
23
  file: string;
24
+ title?: () => string;
22
25
  substitutions?: Map<string, () => string>;
23
26
  links: Array<{link: string, linkTitle: () => string}>;
24
27
  }
@@ -38,8 +41,9 @@ export function resolveLazyDescription(lazyDescription: LazyMarkdownIssueDescrip
38
41
  substitutionMap.set(key, value());
39
42
  });
40
43
 
41
- const description = {
44
+ const description: MarkdownIssueDescription = {
42
45
  file: lazyDescription.file,
46
+ title: lazyDescription.title?.(),
43
47
  links: lazyDescription.links.map(linksMap),
44
48
  substitutions: substitutionMap,
45
49
  };
@@ -54,6 +58,7 @@ export interface IssueDescription {
54
58
  title: string;
55
59
  markdown: Marked.Marked.Token[];
56
60
  links: Array<{link: string, linkTitle: string}>;
61
+ substitutions?: Map<string, string>;
57
62
  }
58
63
 
59
64
  export async function getFileContent(url: URL): Promise<string> {
@@ -71,9 +76,8 @@ export async function getMarkdownFileContent(filename: string): Promise<string>
71
76
 
72
77
  export async function createIssueDescriptionFromMarkdown(description: MarkdownIssueDescription):
73
78
  Promise<IssueDescription> {
74
- const rawMarkdown = await getMarkdownFileContent(description.file);
75
- const rawMarkdownWithPlaceholdersReplaced = substitutePlaceholders(rawMarkdown, description.substitutions);
76
- return createIssueDescriptionFromRawMarkdown(rawMarkdownWithPlaceholdersReplaced, description);
79
+ const markdown = await getMarkdownFileContent(description.file);
80
+ return createIssueDescriptionFromRawMarkdown(markdown, description);
77
81
  }
78
82
 
79
83
  /**
@@ -81,62 +85,20 @@ export async function createIssueDescriptionFromMarkdown(description: MarkdownIs
81
85
  */
82
86
  export function createIssueDescriptionFromRawMarkdown(
83
87
  markdown: string, description: MarkdownIssueDescription): IssueDescription {
84
- const markdownAst = Marked.Marked.lexer(markdown);
85
- const title = findTitleFromMarkdownAst(markdownAst);
86
- if (!title) {
88
+ const markdownAst = TextUtils.Markdown.tokenizeWithPlaceholders(markdown, description.substitutions);
89
+ const markdownTitle = findTitleFromMarkdownAst(markdownAst);
90
+ if (!markdownTitle) {
87
91
  throw new Error('Markdown issue descriptions must start with a heading');
88
92
  }
89
93
 
90
94
  return {
91
- title,
95
+ title: description.title ?? markdownTitle,
92
96
  markdown: markdownAst.slice(1),
93
97
  links: description.links,
98
+ substitutions: description.substitutions,
94
99
  };
95
100
  }
96
101
 
97
- const validPlaceholderMatchPattern = /\{(PLACEHOLDER_[a-zA-Z][a-zA-Z0-9]*)\}/g;
98
- const validPlaceholderNamePattern = /PLACEHOLDER_[a-zA-Z][a-zA-Z0-9]*/;
99
-
100
- /**
101
- * Replaces placeholders in markdown text with a string provided by the
102
- * `substitutions` map. To keep mental overhead to a minimum, the same
103
- * syntax is used as for l10n placeholders. Please note that the
104
- * placeholders require a mandatory 'PLACEHOLDER_' prefix.
105
- *
106
- * Example:
107
- * const str = "This is markdown with `code` and two placeholders, namely {PLACEHOLDER_PH1} and {PLACEHOLDER_PH2}".
108
- * const result = substitutePlaceholders(str, new Map([['PLACEHOLDER_PH1', 'foo'], ['PLACEHOLDER_PH2', 'bar']]));
109
- *
110
- * Exported only for unit testing.
111
- */
112
- export function substitutePlaceholders(markdown: string, substitutions?: Map<string, string>): string {
113
- const unusedPlaceholders = new Set(substitutions ? substitutions.keys() : []);
114
- validatePlaceholders(unusedPlaceholders);
115
-
116
- const result = markdown.replace(validPlaceholderMatchPattern, (_, placeholder) => {
117
- const replacement = substitutions ? substitutions.get(placeholder) : undefined;
118
- if (replacement === undefined) {
119
- throw new Error(`No replacement provided for placeholder '${placeholder}'.`);
120
- }
121
- unusedPlaceholders.delete(placeholder);
122
- return replacement;
123
- });
124
-
125
- if (unusedPlaceholders.size > 0) {
126
- throw new Error(`Unused replacements provided: ${[...unusedPlaceholders]}`);
127
- }
128
-
129
- return result;
130
- }
131
-
132
- /** Ensure that all provided placeholders match the naming pattern. **/
133
- function validatePlaceholders(placeholders: Set<string>): void {
134
- const invalidPlaceholders = [...placeholders].filter(placeholder => !validPlaceholderNamePattern.test(placeholder));
135
- if (invalidPlaceholders.length > 0) {
136
- throw new Error(`Invalid placeholders provided in the substitutions map: ${invalidPlaceholders}`);
137
- }
138
- }
139
-
140
102
  export function findTitleFromMarkdownAst(markdownAst: Marked.Marked.Token[]): string|null {
141
103
  if (markdownAst.length === 0 || markdownAst[0].type !== 'heading' || markdownAst[0].depth !== 1) {
142
104
  return null;
@@ -148,5 +110,9 @@ export async function getIssueTitleFromMarkdownDescription(description: Markdown
148
110
  Promise<string|null> {
149
111
  const rawMarkdown = await getMarkdownFileContent(description.file);
150
112
  const markdownAst = Marked.Marked.lexer(rawMarkdown);
151
- return findTitleFromMarkdownAst(markdownAst);
113
+ const markdownTitle = findTitleFromMarkdownAst(markdownAst);
114
+ if (!markdownTitle) {
115
+ return null;
116
+ }
117
+ return description.title ?? markdownTitle;
152
118
  }
@@ -77,10 +77,8 @@ export class SRIMessageSignatureIssue extends Issue<Protocol.Audits.SRIMessageSi
77
77
  description.substitutions?.set('PLACEHOLDER_signatureBase', () => details.signatureBase);
78
78
  }
79
79
  if (details.error === Protocol.Audits.SRIMessageSignatureError.ValidationFailedIntegrityMismatch) {
80
- description.substitutions?.set('PLACEHOLDER_integrityAssertions', () => {
81
- const prefix = '\n* ';
82
- return prefix + this.details().integrityAssertions.join(prefix);
83
- });
80
+ description.substitutions?.set('PLACEHOLDER_integrityAssertions',
81
+ () => this.details().integrityAssertions.join('\n'));
84
82
  }
85
83
  return resolveLazyDescription(description);
86
84
  }
@@ -1,4 +1,3 @@
1
1
  # Third-party websites are allowed to read cookies on this page
2
2
 
3
3
  One or more websites are allowed to bypass user settings to read third-party cookies on this page. Web developers should take steps to remove these reads without disrupting user experience. To forcefully block these third-party cookies, update [user settings](manageCookiesHelpPage).
4
- {PLACEHOLDER_topleveloptout}
@@ -0,0 +1,5 @@
1
+ # Third-party websites are allowed to read cookies on this page
2
+
3
+ One or more websites are allowed to bypass user settings to read third-party cookies on this page. Web developers should take steps to remove these reads without disrupting user experience. To forcefully block these third-party cookies, update [user settings](manageCookiesHelpPage).
4
+
5
+ (Top level site opt-out: {PLACEHOLDER_optOutPercentage}% - [learn more](gracePeriodStagedControlExplainer))
@@ -1,4 +1,3 @@
1
1
  # Third-party websites are allowed to set cookies on this page
2
2
 
3
3
  One or more websites are allowed to bypass user settings to set third-party cookies on this page. Web developers should take steps to remove these sets without disrupting user experience. To forcefully block these third-party cookies, update [user settings](manageCookiesHelpPage).
4
- {PLACEHOLDER_topleveloptout}
@@ -0,0 +1,5 @@
1
+ # Third-party websites are allowed to set cookies on this page
2
+
3
+ One or more websites are allowed to bypass user settings to set third-party cookies on this page. Web developers should take steps to remove these sets without disrupting user experience. To forcefully block these third-party cookies, update [user settings](manageCookiesHelpPage).
4
+
5
+ (Top level site opt-out: {PLACEHOLDER_optOutPercentage}% - [learn more](gracePeriodStagedControlExplainer))
@@ -1,3 +1,3 @@
1
- # {PLACEHOLDER_title}
1
+ # Deprecated feature used
2
2
 
3
3
  {PLACEHOLDER_message}
@@ -7,4 +7,6 @@ assertions made by the request’s initiator. Verification failed.
7
7
 
8
8
  The following are the keys specified by the request’s initiator:
9
9
 
10
+ ```
10
11
  {PLACEHOLDER_integrityAssertions}
12
+ ```
@@ -64,7 +64,7 @@ export class FileSystem implements Workspace.Workspace.Project {
64
64
  }
65
65
 
66
66
  displayName(): string {
67
- const {root} = this.automaticFileSystem;
67
+ const root = this.automaticFileSystem.root.replace(Host.Platform.isWin() ? /[/\\]+$/ : /\/+$/, '');
68
68
  let slash = root.lastIndexOf('/');
69
69
  if (slash === -1 && Host.Platform.isWin()) {
70
70
  slash = root.lastIndexOf('\\');
@@ -270,6 +270,8 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
270
270
  encodeURI(pathPart).replace(/[\/\*]/g, match => '%' + match[0].charCodeAt(0).toString(16).toUpperCase());
271
271
  if (encodedName === '..') {
272
272
  encodedName = '%2E%2E';
273
+ } else if (encodedName === '.') {
274
+ encodedName = '%2E';
273
275
  }
274
276
  if (Host.Platform.isWin()) {
275
277
  // Windows does not allow ':' and '?' in filenames
@@ -335,7 +337,10 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
335
337
  if (!encodedFilePath) {
336
338
  return null;
337
339
  }
338
- const encodedPath = Common.ParsedURL.ParsedURL.substring(encodedFilePath, 0, encodedFilePath.lastIndexOf('/'));
340
+ const encodedPath = Common.ParsedURL.ParsedURL.substr(encodedFilePath, 0, encodedFilePath.lastIndexOf('/'));
341
+ if (!encodedPath) {
342
+ return null;
343
+ }
339
344
  uiSourceCode = await this.#project.createFile(encodedPath, HEADERS_FILENAME, '');
340
345
  Host.userMetrics.actionTaken(Host.UserMetrics.Action.HeaderOverrideFileCreated);
341
346
  }
@@ -525,6 +530,10 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
525
530
  const encodedFileName = Common.ParsedURL.ParsedURL.substring(encodedPath, lastIndexOfSlash + 1);
526
531
  const rawFileName = Common.ParsedURL.ParsedURL.encodedPathToRawPathString(encodedFileName);
527
532
  encodedPath = Common.ParsedURL.ParsedURL.substr(encodedPath, 0, lastIndexOfSlash);
533
+ if (!encodedPath || rawFileName === HEADERS_FILENAME) {
534
+ this.#savingForOverrides.delete(uiSourceCode);
535
+ return;
536
+ }
528
537
  if (this.#project) {
529
538
  await this.#project.createFile(encodedPath, rawFileName, content ?? '', isEncoded);
530
539
  }
@@ -564,7 +573,7 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
564
573
  const relativePathParts = FileSystemWorkspaceBinding.relativePath(uiSourceCode);
565
574
  // Decode twice to handle paths generated on Windows OS.
566
575
  const host = this.decodeLocalPathToUrlPath(this.decodeLocalPathToUrlPath(relativePathParts[0] || '')).toLowerCase();
567
- return ['chrome:', 'data:', 'blob:', 'javascript:', 'about:', 'mailto:', 'vbscript:'].includes(host) ||
576
+ return ['chrome:', 'data:', 'blob:', 'javascript:', 'about:', 'mailto:', 'vbscript:', '.', '..'].includes(host) ||
568
577
  forbiddenUrls.includes(host);
569
578
  }
570
579
 
@@ -578,6 +587,9 @@ export class NetworkPersistenceManager extends Common.ObjectWrapper.ObjectWrappe
578
587
  if (!url) {
579
588
  return false;
580
589
  }
590
+ if ((url.scheme === 'http' || url.scheme === 'https') && (!url.host || url.host === '.' || url.host === '..')) {
591
+ return true;
592
+ }
581
593
  return !['http', 'https', 'file'].includes(url.scheme) || forbiddenUrls.includes(url.host);
582
594
  }
583
595
 
@@ -7,6 +7,7 @@ import '../../ui/components/tooltips/tooltips.js';
7
7
  import * as i18n from '../../core/i18n/i18n.js';
8
8
  import type * as SDK from '../../core/sdk/sdk.js';
9
9
  import type * as CommentManager from '../../models/comment_manager/comment_manager.js';
10
+ import * as Buttons from '../../ui/components/buttons/buttons.js';
10
11
  import * as Input from '../../ui/components/input/input.js';
11
12
  import * as MarkdownView from '../../ui/components/markdown_view/markdown_view.js';
12
13
  import * as UI from '../../ui/legacy/legacy.js';
@@ -41,6 +42,10 @@ const UIStrings = {
41
42
  * @description aria-label for the comment text area.
42
43
  */
43
44
  commentInputAriaLabel: 'Comment input',
45
+ /**
46
+ * @description Tooltip and aria-label for the close button in the comment thread header.
47
+ */
48
+ close: 'Close',
44
49
  } as const;
45
50
 
46
51
  const UIStringsNotTranslate = {
@@ -70,6 +75,7 @@ export interface ViewInput {
70
75
  textAreaRef: Lit.Directives.Ref<HTMLTextAreaElement>;
71
76
  onAddComment: (text: string) => void;
72
77
  onCommentTextChange: (event: Event) => void;
78
+ onClose?: () => void;
73
79
  }
74
80
 
75
81
  export type ViewOutput = undefined;
@@ -87,16 +93,27 @@ export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTML
87
93
  widget(DOMNodeLink, {node: input.title.node}) :
88
94
  html`<span class="selected-item-text">${input.title.text}</span>`}
89
95
  </span>
90
- ${hasComment ? html`
91
- <div class="sent-status">
92
- <devtools-icon
93
- class="check-icon"
94
- name="checkmark"
95
- aria-label=${i18nString(UIStrings.sentCheckmark)}>
96
- </devtools-icon>
97
- <span>${i18nString(UIStrings.sent)}</span>
98
- </div>
99
- ` : Lit.nothing}
96
+ <div class="header-actions">
97
+ ${hasComment ? html`
98
+ <div class="sent-status">
99
+ <devtools-icon
100
+ class="check-icon"
101
+ name="checkmark"
102
+ aria-label=${i18nString(UIStrings.sentCheckmark)}>
103
+ </devtools-icon>
104
+ <span>${i18nString(UIStrings.sent)}</span>
105
+ </div>
106
+ ` : Lit.nothing}
107
+ <devtools-button
108
+ class="close-button"
109
+ aria-label=${i18nString(UIStrings.close)}
110
+ .iconName=${'cross'}
111
+ .variant=${Buttons.Button.Variant.ICON}
112
+ .size=${Buttons.Button.Size.SMALL}
113
+ .title=${i18nString(UIStrings.close)}
114
+ @click=${input.onClose}
115
+ ></devtools-button>
116
+ </div>
100
117
  </div>
101
118
 
102
119
  ${hasComment ? html`
@@ -171,6 +188,7 @@ export class CommentThreadWidget extends UI.Widget.Widget {
171
188
  #textAreaRef = createRef<HTMLTextAreaElement>();
172
189
  #view: View;
173
190
  onAddComment?: (text: string) => void;
191
+ onClose?: () => void;
174
192
 
175
193
  constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
176
194
  super(element);
@@ -213,6 +231,7 @@ export class CommentThreadWidget extends UI.Widget.Widget {
213
231
  textAreaRef: this.#textAreaRef,
214
232
  onAddComment: this.#handleAddComment,
215
233
  onCommentTextChange: this.#handleCommentTextChange,
234
+ onClose: this.onClose,
216
235
  };
217
236
  this.#view(viewInput, undefined, this.contentElement);
218
237
  }
@@ -31,6 +31,7 @@ export interface ViewInput {
31
31
  activePin: Comments.CommentOverlayManager.PinPositionData|null;
32
32
  title: Title;
33
33
  onAddComment: (text: string) => void;
34
+ onCloseCommentThread: () => void;
34
35
  }
35
36
 
36
37
  export type View = (
@@ -103,6 +104,7 @@ const DEFAULT_VIEW: View = (input: ViewInput, _output: undefined, target: HTMLEl
103
104
  title: input.title,
104
105
  comments: [...item.thread.comments],
105
106
  onAddComment: input.onAddComment,
107
+ onClose: input.onCloseCommentThread,
106
108
  })}
107
109
  </div>
108
110
  `,
@@ -124,6 +126,18 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
124
126
  #cachedTitle: Title = {text: ''};
125
127
  #cachedTitleAnchor: CommentManager.CommentManager.CommentAnchorSignature|null = null;
126
128
 
129
+ #setActiveThreadId(threadId: string|null): void {
130
+ if (this.#activeThreadId !== threadId) {
131
+ this.#clearCloseTimeout();
132
+ this.#activeThreadId = threadId;
133
+ if (threadId && this.isShowing()) {
134
+ document.documentElement.addEventListener('keydown', this.#onKeyDown);
135
+ } else {
136
+ document.documentElement.removeEventListener('keydown', this.#onKeyDown);
137
+ }
138
+ }
139
+ }
140
+
127
141
  constructor(
128
142
  element: HTMLElement|undefined,
129
143
  [commentManager]: UI.Widget.WidgetDependencies<typeof CommentsOverlayWidget>,
@@ -170,12 +184,16 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
170
184
  this.#onAgentAttachedChanged,
171
185
  this,
172
186
  );
187
+ if (this.#activeThreadId) {
188
+ document.documentElement.addEventListener('keydown', this.#onKeyDown);
189
+ }
173
190
 
174
191
  this.requestUpdate();
175
192
  }
176
193
 
177
194
  override willHide(): void {
178
195
  this.#clearCloseTimeout();
196
+ document.documentElement.removeEventListener('keydown', this.#onKeyDown);
179
197
  this.#commentOverlayManager.stop();
180
198
  this.#commentOverlayManager.removeEventListener(
181
199
  Comments.CommentOverlayManager.Events.POSITIONS_UPDATED,
@@ -219,7 +237,7 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
219
237
  CommentManager.CommentManager.EventTypes[CommentManager.CommentManager.Events.AGENT_ATTACHED_CHANGED]>,
220
238
  ): void {
221
239
  if (!event.data) {
222
- this.#activeThreadId = null;
240
+ this.#setActiveThreadId(null);
223
241
  }
224
242
  this.requestUpdate();
225
243
  }
@@ -230,7 +248,7 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
230
248
  ): void {
231
249
  const isModeActive = event.data;
232
250
  if (!isModeActive) {
233
- this.#activeThreadId = null;
251
+ this.#setActiveThreadId(null);
234
252
  }
235
253
  const action = UI.ActionRegistry.ActionRegistry.instance().getAction(
236
254
  'comments.toggle-comment-mode',
@@ -242,9 +260,9 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
242
260
  #onStateChanged(): void {
243
261
  const draftThread = this.#commentManager.getCommentThreads().find(t => t.status === 'DRAFT');
244
262
  if (draftThread) {
245
- this.#activeThreadId = draftThread.id;
263
+ this.#setActiveThreadId(draftThread.id);
246
264
  } else if (this.#activeThreadId && !this.#commentManager.getCommentThread(this.#activeThreadId)) {
247
- this.#activeThreadId = null;
265
+ this.#setActiveThreadId(null);
248
266
  }
249
267
  this.requestUpdate();
250
268
  }
@@ -272,7 +290,6 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
272
290
  return {node};
273
291
  }
274
292
  }
275
- return {text: ''};
276
293
  }
277
294
 
278
295
  if (anchor.networkRequestId) {
@@ -287,19 +304,35 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
287
304
  }
288
305
 
289
306
  #handlePinClick = (threadId: string): void => {
290
- const thread = this.#commentManager.getCommentThread(threadId);
291
307
  if (this.#activeThreadId === threadId) {
292
- if (thread?.status === 'DRAFT') {
293
- this.#commentOverlayManager.clearDraftThreads();
294
- }
295
- this.#activeThreadId = null;
296
- } else {
308
+ this.#handleCloseCommentThread();
309
+ return;
310
+ }
311
+
312
+ this.#commentOverlayManager.clearDraftThreads();
313
+ this.#setActiveThreadId(threadId);
314
+ this.requestUpdate();
315
+ };
316
+
317
+ #handleCloseCommentThread = (): void => {
318
+ if (!this.#activeThreadId) {
319
+ return;
320
+ }
321
+ const thread = this.#commentManager.getCommentThread(this.#activeThreadId);
322
+ if (thread?.status === 'DRAFT') {
297
323
  this.#commentOverlayManager.clearDraftThreads();
298
- this.#activeThreadId = threadId;
299
324
  }
325
+ this.#setActiveThreadId(null);
300
326
  this.requestUpdate();
301
327
  };
302
328
 
329
+ #onKeyDown = (event: KeyboardEvent): void => {
330
+ if (this.#activeThreadId && event.key === 'Escape' && !event.isComposing) {
331
+ event.consume(true);
332
+ this.#handleCloseCommentThread();
333
+ }
334
+ };
335
+
303
336
  override async performUpdate(signal?: AbortSignal): Promise<void> {
304
337
  if (!this.#commentManager.isAgentAttached()) {
305
338
  this.#view(
@@ -313,6 +346,7 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
313
346
  activePin: null,
314
347
  title: {text: ''},
315
348
  onAddComment: () => {},
349
+ onCloseCommentThread: this.#handleCloseCommentThread,
316
350
  },
317
351
  undefined,
318
352
  this.contentElement,
@@ -342,16 +376,18 @@ export class CommentsOverlayWidget extends UI.Widget.Widget {
342
376
  if (!activeThread) {
343
377
  return;
344
378
  }
345
- activeThread.save(text);
379
+ activeThread.sendToAgent(text);
346
380
  const threadId = activeThread.id;
381
+ this.#clearCloseTimeout();
347
382
  this.#closeTimeoutId = window.setTimeout(() => {
348
383
  this.#closeTimeoutId = null;
349
384
  if (this.#activeThreadId === threadId) {
350
- this.#activeThreadId = null;
385
+ this.#setActiveThreadId(null);
351
386
  this.requestUpdate();
352
387
  }
353
388
  }, AUTO_CLOSE_DELAY_MS);
354
389
  },
390
+ onCloseCommentThread: this.#handleCloseCommentThread,
355
391
  };
356
392
  this.#view(viewInput, undefined, this.contentElement);
357
393
  }