chrome-devtools-frontend 1.0.1702515 → 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.
- package/front_end/core/common/ParsedURL.ts +5 -1
- package/front_end/core/text_utils/Markdown.ts +73 -0
- package/front_end/core/text_utils/text_utils.ts +2 -0
- package/front_end/generated/InspectorBackendCommands.ts +3 -1
- package/front_end/generated/protocol-mapping.d.ts +8 -0
- package/front_end/generated/protocol-proxy-api.d.ts +6 -0
- package/front_end/generated/protocol.ts +22 -0
- package/front_end/models/comment_manager/CD4ABridge.ts +5 -0
- package/front_end/models/issues_manager/MarkdownIssueDescription.ts +4 -47
- package/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts +1 -1
- package/front_end/panels/common/CommentsStatusBarPill.ts +40 -7
- package/front_end/panels/common/commentsStatusBarPill.css +13 -0
- package/front_end/panels/issues/IssueView.ts +5 -1
- package/front_end/panels/sources/SourcesView.ts +127 -147
- package/front_end/panels/sources/TabbedEditorContainer.ts +266 -207
- package/front_end/third_party/chromium/README.chromium +1 -1
- package/front_end/ui/components/markdown_view/MarkdownPlaceholderLitRenderer.ts +7 -65
- package/front_end/ui/legacy/TabbedPane.ts +51 -40
- package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +342 -373
- package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
- package/package.json +2 -1
|
@@ -179,7 +179,11 @@ export class ParsedURL {
|
|
|
179
179
|
preEncodedPath = 'file:///' + preEncodedPath;
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
-
|
|
182
|
+
let url = new URL(preEncodedPath).toString();
|
|
183
|
+
while (url.endsWith('/') && url.length > 'file:///'.length) {
|
|
184
|
+
url = url.substring(0, url.length - 1);
|
|
185
|
+
}
|
|
186
|
+
return url as Platform.DevToolsPath.UrlString;
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
static relativePathToUrlString(
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright 2026 The Chromium Authors
|
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
|
3
|
+
// found in the LICENSE file.
|
|
4
|
+
|
|
5
|
+
import * as Marked from '../../third_party/marked/marked.js';
|
|
6
|
+
|
|
7
|
+
export interface PlaceholderToken extends Marked.Marked.Tokens.Generic {
|
|
8
|
+
type: 'placeholder';
|
|
9
|
+
raw: string;
|
|
10
|
+
key: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const VALID_PLACEHOLDER_MATCH_PATTERN: RegExp = /\{(PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*)\}/g;
|
|
14
|
+
const VALID_PLACEHOLDER_NAME_PATTERN = /^PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
15
|
+
|
|
16
|
+
const placeholderExtension: Marked.Marked.TokenizerExtension = {
|
|
17
|
+
name: 'placeholder',
|
|
18
|
+
level: 'inline',
|
|
19
|
+
start(src: string): number |
|
|
20
|
+
undefined {
|
|
21
|
+
return src.match(/\{PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*\}/)?.index;
|
|
22
|
+
},
|
|
23
|
+
tokenizer(src: string): PlaceholderToken |
|
|
24
|
+
undefined {
|
|
25
|
+
const match = /^\{(PLACEHOLDER_[a-zA-Z][a-zA-Z0-9_]*)\}/.exec(src);
|
|
26
|
+
if (match) {
|
|
27
|
+
return {
|
|
28
|
+
type: 'placeholder',
|
|
29
|
+
raw: match[0],
|
|
30
|
+
key: match[1],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function validateSubstitutions(rawMarkdown: string, substitutions?: Map<string, string>): void {
|
|
38
|
+
if (!substitutions) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const unusedPlaceholders = new Set(substitutions.keys());
|
|
42
|
+
for (const key of unusedPlaceholders) {
|
|
43
|
+
if (!VALID_PLACEHOLDER_NAME_PATTERN.test(key)) {
|
|
44
|
+
throw new Error(`Invalid placeholder '${key}' provided in the substitutions map.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [, placeholder] of rawMarkdown.matchAll(VALID_PLACEHOLDER_MATCH_PATTERN)) {
|
|
48
|
+
if (!substitutions.has(placeholder)) {
|
|
49
|
+
throw new Error(`No replacement provided for placeholder '${placeholder}'.`);
|
|
50
|
+
}
|
|
51
|
+
unusedPlaceholders.delete(placeholder);
|
|
52
|
+
}
|
|
53
|
+
if (unusedPlaceholders.size > 0) {
|
|
54
|
+
throw new Error(`Unused replacements provided: ${[...unusedPlaceholders]}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tokenizes markdown into a Marked AST while preserving `{PLACEHOLDER_*}` tokens
|
|
60
|
+
* as `PlaceholderToken` AST nodes instead of substituting strings before lexing.
|
|
61
|
+
*/
|
|
62
|
+
export function tokenizeWithPlaceholders(markdown: string|Marked.Marked.Token[],
|
|
63
|
+
substitutions?: Map<string, string>): Marked.Marked.Token[] {
|
|
64
|
+
const rawMarkdown = typeof markdown === 'string' ? markdown : markdown.map(token => token.raw).join('');
|
|
65
|
+
validateSubstitutions(rawMarkdown, substitutions);
|
|
66
|
+
if (typeof markdown !== 'string' && !rawMarkdown.includes('{PLACEHOLDER_')) {
|
|
67
|
+
return markdown;
|
|
68
|
+
}
|
|
69
|
+
const markedInstance = new Marked.Marked.Marked({
|
|
70
|
+
extensions: [placeholderExtension],
|
|
71
|
+
});
|
|
72
|
+
return markedInstance.lexer(rawMarkdown);
|
|
73
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as CodeMirrorUtils from './CodeMirrorUtils.js';
|
|
6
6
|
import * as ContentData from './ContentData.js';
|
|
7
7
|
import * as ContentProvider from './ContentProvider.js';
|
|
8
|
+
import * as Markdown from './Markdown.js';
|
|
8
9
|
import * as StaticContentProvider from './StaticContentProvider.js';
|
|
9
10
|
import * as StreamingContentData from './StreamingContentData.js';
|
|
10
11
|
import * as Text from './Text.js';
|
|
@@ -17,6 +18,7 @@ export {
|
|
|
17
18
|
CodeMirrorUtils,
|
|
18
19
|
ContentData,
|
|
19
20
|
ContentProvider,
|
|
21
|
+
Markdown,
|
|
20
22
|
StaticContentProvider,
|
|
21
23
|
StreamingContentData,
|
|
22
24
|
Text,
|
|
@@ -1183,7 +1183,7 @@ inspectorBackend.registerType("Page.FrameResource", [{"name": "url", "type": "st
|
|
|
1183
1183
|
inspectorBackend.registerType("Page.FrameResourceTree", [{"name": "frame", "type": "object", "optional": false, "description": "Frame information for this tree item.", "typeRef": "Page.Frame"}, {"name": "childFrames", "type": "array", "optional": true, "description": "Child frames.", "typeRef": "Page.FrameResourceTree"}, {"name": "resources", "type": "array", "optional": false, "description": "Information about frame resources.", "typeRef": "Page.FrameResource"}]);
|
|
1184
1184
|
inspectorBackend.registerType("Page.FrameTree", [{"name": "frame", "type": "object", "optional": false, "description": "Frame information for this tree item.", "typeRef": "Page.Frame"}, {"name": "childFrames", "type": "array", "optional": true, "description": "Child frames.", "typeRef": "Page.FrameTree"}]);
|
|
1185
1185
|
inspectorBackend.registerType("Page.NavigationEntry", [{"name": "id", "type": "number", "optional": false, "description": "Unique id of the navigation history entry.", "typeRef": null}, {"name": "url", "type": "string", "optional": false, "description": "URL of the navigation history entry.", "typeRef": null}, {"name": "userTypedURL", "type": "string", "optional": false, "description": "URL that the user typed in the url bar.", "typeRef": null}, {"name": "title", "type": "string", "optional": false, "description": "Title of the navigation history entry.", "typeRef": null}, {"name": "transitionType", "type": "string", "optional": false, "description": "Transition type.", "typeRef": "Page.TransitionType"}]);
|
|
1186
|
-
inspectorBackend.registerType("Page.ScreencastFrameMetadata", [{"name": "offsetTop", "type": "number", "optional": false, "description": "Top offset in DIP.", "typeRef": null}, {"name": "pageScaleFactor", "type": "number", "optional": false, "description": "Page scale factor.", "typeRef": null}, {"name": "deviceWidth", "type": "number", "optional": false, "description": "Device screen width in DIP.", "typeRef": null}, {"name": "deviceHeight", "type": "number", "optional": false, "description": "Device screen height in DIP.", "typeRef": null}, {"name": "scrollOffsetX", "type": "number", "optional": false, "description": "Position of horizontal scroll in CSS pixels.", "typeRef": null}, {"name": "scrollOffsetY", "type": "number", "optional": false, "description": "Position of vertical scroll in CSS pixels.", "typeRef": null}, {"name": "timestamp", "type": "number", "optional": true, "description": "Frame swap timestamp.", "typeRef": "Network.TimeSinceEpoch"}]);
|
|
1186
|
+
inspectorBackend.registerType("Page.ScreencastFrameMetadata", [{"name": "offsetTop", "type": "number", "optional": false, "description": "Top offset in DIP.", "typeRef": null}, {"name": "pageScaleFactor", "type": "number", "optional": false, "description": "Page scale factor.", "typeRef": null}, {"name": "deviceWidth", "type": "number", "optional": false, "description": "Device screen width in DIP.", "typeRef": null}, {"name": "deviceHeight", "type": "number", "optional": false, "description": "Device screen height in DIP.", "typeRef": null}, {"name": "scrollOffsetX", "type": "number", "optional": false, "description": "Position of horizontal scroll in CSS pixels.", "typeRef": null}, {"name": "scrollOffsetY", "type": "number", "optional": false, "description": "Position of vertical scroll in CSS pixels.", "typeRef": null}, {"name": "timestamp", "type": "number", "optional": true, "description": "Frame swap timestamp.", "typeRef": "Network.TimeSinceEpoch"}, {"name": "monotonicTimestamp", "type": "number", "optional": true, "description": "Frame swap timestamp as monotonic time.", "typeRef": "Network.MonotonicTime"}]);
|
|
1187
1187
|
inspectorBackend.registerType("Page.AppManifestError", [{"name": "message", "type": "string", "optional": false, "description": "Error message.", "typeRef": null}, {"name": "critical", "type": "number", "optional": false, "description": "If critical, this is a non-recoverable parse error.", "typeRef": null}, {"name": "line", "type": "number", "optional": false, "description": "Error line.", "typeRef": null}, {"name": "column", "type": "number", "optional": false, "description": "Error column.", "typeRef": null}]);
|
|
1188
1188
|
inspectorBackend.registerType("Page.AppManifestParsedProperties", [{"name": "scope", "type": "string", "optional": false, "description": "Computed scope value", "typeRef": null}]);
|
|
1189
1189
|
inspectorBackend.registerType("Page.LayoutViewport", [{"name": "pageX", "type": "number", "optional": false, "description": "Horizontal offset relative to the document (CSS pixels).", "typeRef": null}, {"name": "pageY", "type": "number", "optional": false, "description": "Vertical offset relative to the document (CSS pixels).", "typeRef": null}, {"name": "clientWidth", "type": "number", "optional": false, "description": "Width (CSS pixels), excludes scrollbar if present.", "typeRef": null}, {"name": "clientHeight", "type": "number", "optional": false, "description": "Height (CSS pixels), excludes scrollbar if present.", "typeRef": null}]);
|
|
@@ -1362,6 +1362,7 @@ inspectorBackend.registerCommand("Storage.untrackIndexedDBForStorageKey", [{"nam
|
|
|
1362
1362
|
inspectorBackend.registerCommand("Storage.getTrustTokens", [], ["tokens"], "Returns the number of stored Trust Tokens per issuer for the current browsing context.");
|
|
1363
1363
|
inspectorBackend.registerCommand("Storage.clearTrustTokens", [{"name": "issuerOrigin", "type": "string", "optional": false, "description": "", "typeRef": null}], ["didDeleteTokens"], "Removes all Trust Tokens issued by the provided issuerOrigin. Leaves other stored data, including the issuer's Redemption Records, intact.");
|
|
1364
1364
|
inspectorBackend.registerCommand("Storage.getPrivateVerificationTokens", [], ["tokens"], "Returns all stored Private Verification Tokens for the current browsing context.");
|
|
1365
|
+
inspectorBackend.registerCommand("Storage.getPrivateVerificationTokensIssuerConfigs", [], ["configs"], "Returns the configured Private Verification Tokens issuers and their redeemer origins.");
|
|
1365
1366
|
inspectorBackend.registerCommand("Storage.clearPrivateVerificationTokens", [{"name": "issuerOrigin", "type": "string", "optional": false, "description": "", "typeRef": null}], [], "Removes all Private Verification Tokens issued by the provided issuerOrigin.");
|
|
1366
1367
|
inspectorBackend.registerCommand("Storage.deletePrivateVerificationToken", [{"name": "tokenId", "type": "string", "optional": false, "description": "", "typeRef": null}], [], "Removes a specific Private Verification Token by its ID.");
|
|
1367
1368
|
inspectorBackend.registerCommand("Storage.setPrivateVerificationTokensTracking", [{"name": "enable", "type": "boolean", "optional": false, "description": "", "typeRef": null}], [], "Set tracking for Private Verification Tokens.");
|
|
@@ -1371,6 +1372,7 @@ inspectorBackend.registerCommand("Storage.runBounceTrackingMitigations", [], ["d
|
|
|
1371
1372
|
inspectorBackend.registerType("Storage.UsageForType", [{"name": "storageType", "type": "string", "optional": false, "description": "Name of storage type.", "typeRef": "Storage.StorageType"}, {"name": "usage", "type": "number", "optional": false, "description": "Storage usage (bytes).", "typeRef": null}]);
|
|
1372
1373
|
inspectorBackend.registerType("Storage.TrustTokens", [{"name": "issuerOrigin", "type": "string", "optional": false, "description": "", "typeRef": null}, {"name": "count", "type": "number", "optional": false, "description": "", "typeRef": null}]);
|
|
1373
1374
|
inspectorBackend.registerType("Storage.PrivateVerificationToken", [{"name": "id", "type": "string", "optional": false, "description": "Unique identifier of the token in the database.", "typeRef": null}, {"name": "issuerOrigin", "type": "string", "optional": false, "description": "Origin of the token issuer.", "typeRef": null}, {"name": "keyId", "type": "number", "optional": false, "description": "Public key ID used to issue the token.", "typeRef": null}, {"name": "expiration", "type": "number", "optional": false, "description": "Expiration timestamp in seconds since the epoch.", "typeRef": "Network.TimeSinceEpoch"}, {"name": "creationTime", "type": "number", "optional": false, "description": "Token creation timestamp in seconds since the epoch.", "typeRef": "Network.TimeSinceEpoch"}, {"name": "version", "type": "number", "optional": false, "description": "Token protocol version.", "typeRef": null}, {"name": "token", "type": "string", "optional": false, "description": "Base64-encoded serialized token.", "typeRef": null}]);
|
|
1375
|
+
inspectorBackend.registerType("Storage.PrivateVerificationTokensIssuerConfig", [{"name": "issuerOrigin", "type": "string", "optional": false, "description": "Origin of the token issuer.", "typeRef": null}, {"name": "redeemerOrigins", "type": "array", "optional": false, "description": "Origins authorized to redeem tokens from this issuer.", "typeRef": "string"}]);
|
|
1374
1376
|
inspectorBackend.registerType("Storage.StorageBucket", [{"name": "storageKey", "type": "string", "optional": false, "description": "", "typeRef": "Storage.SerializedStorageKey"}, {"name": "name", "type": "string", "optional": true, "description": "If not specified, it is the default bucket of the storageKey.", "typeRef": null}]);
|
|
1375
1377
|
inspectorBackend.registerType("Storage.StorageBucketInfo", [{"name": "bucket", "type": "object", "optional": false, "description": "", "typeRef": "Storage.StorageBucket"}, {"name": "id", "type": "string", "optional": false, "description": "", "typeRef": null}, {"name": "expiration", "type": "number", "optional": false, "description": "", "typeRef": "Network.TimeSinceEpoch"}, {"name": "quota", "type": "number", "optional": false, "description": "Storage quota (bytes).", "typeRef": null}, {"name": "persistent", "type": "boolean", "optional": false, "description": "", "typeRef": null}, {"name": "durability", "type": "string", "optional": false, "description": "", "typeRef": "Storage.StorageBucketsDurability"}]);
|
|
1376
1378
|
|
|
@@ -4954,6 +4954,14 @@ export namespace ProtocolMapping {
|
|
|
4954
4954
|
paramsType: [];
|
|
4955
4955
|
returnType: Protocol.Storage.GetPrivateVerificationTokensResponse;
|
|
4956
4956
|
};
|
|
4957
|
+
/**
|
|
4958
|
+
* Returns the configured Private Verification Tokens issuers and their redeemer
|
|
4959
|
+
* origins.
|
|
4960
|
+
*/
|
|
4961
|
+
'Storage.getPrivateVerificationTokensIssuerConfigs': {
|
|
4962
|
+
paramsType: [];
|
|
4963
|
+
returnType: Protocol.Storage.GetPrivateVerificationTokensIssuerConfigsResponse;
|
|
4964
|
+
};
|
|
4957
4965
|
/**
|
|
4958
4966
|
* Removes all Private Verification Tokens issued by the provided issuerOrigin.
|
|
4959
4967
|
*/
|
|
@@ -4331,6 +4331,12 @@ declare namespace ProtocolProxyApi {
|
|
|
4331
4331
|
*/
|
|
4332
4332
|
invoke_getPrivateVerificationTokens(): Promise<Protocol.Storage.GetPrivateVerificationTokensResponse>;
|
|
4333
4333
|
|
|
4334
|
+
/**
|
|
4335
|
+
* Returns the configured Private Verification Tokens issuers and their redeemer
|
|
4336
|
+
* origins.
|
|
4337
|
+
*/
|
|
4338
|
+
invoke_getPrivateVerificationTokensIssuerConfigs(): Promise<Protocol.Storage.GetPrivateVerificationTokensIssuerConfigsResponse>;
|
|
4339
|
+
|
|
4334
4340
|
/**
|
|
4335
4341
|
* Removes all Private Verification Tokens issued by the provided issuerOrigin.
|
|
4336
4342
|
*/
|
|
@@ -15133,6 +15133,10 @@ export namespace Page {
|
|
|
15133
15133
|
* Frame swap timestamp.
|
|
15134
15134
|
*/
|
|
15135
15135
|
timestamp?: Network.TimeSinceEpoch;
|
|
15136
|
+
/**
|
|
15137
|
+
* Frame swap timestamp as monotonic time.
|
|
15138
|
+
*/
|
|
15139
|
+
monotonicTimestamp?: Network.MonotonicTime;
|
|
15136
15140
|
}
|
|
15137
15141
|
|
|
15138
15142
|
/**
|
|
@@ -18504,6 +18508,20 @@ export namespace Storage {
|
|
|
18504
18508
|
token: string;
|
|
18505
18509
|
}
|
|
18506
18510
|
|
|
18511
|
+
/**
|
|
18512
|
+
* Configuration for a Private Verification Tokens issuer.
|
|
18513
|
+
*/
|
|
18514
|
+
export interface PrivateVerificationTokensIssuerConfig {
|
|
18515
|
+
/**
|
|
18516
|
+
* Origin of the token issuer.
|
|
18517
|
+
*/
|
|
18518
|
+
issuerOrigin: string;
|
|
18519
|
+
/**
|
|
18520
|
+
* Origins authorized to redeem tokens from this issuer.
|
|
18521
|
+
*/
|
|
18522
|
+
redeemerOrigins: string[];
|
|
18523
|
+
}
|
|
18524
|
+
|
|
18507
18525
|
export const enum StorageBucketsDurability {
|
|
18508
18526
|
Relaxed = 'relaxed',
|
|
18509
18527
|
Strict = 'strict',
|
|
@@ -18717,6 +18735,10 @@ export namespace Storage {
|
|
|
18717
18735
|
tokens: PrivateVerificationToken[];
|
|
18718
18736
|
}
|
|
18719
18737
|
|
|
18738
|
+
export interface GetPrivateVerificationTokensIssuerConfigsResponse extends ProtocolResponseWithError {
|
|
18739
|
+
configs: PrivateVerificationTokensIssuerConfig[];
|
|
18740
|
+
}
|
|
18741
|
+
|
|
18720
18742
|
export interface ClearPrivateVerificationTokensRequest {
|
|
18721
18743
|
issuerOrigin: string;
|
|
18722
18744
|
}
|
|
@@ -87,8 +87,13 @@ export class CD4ABridge extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
|
87
87
|
|
|
88
88
|
#formatCommentText(thread: CommentManager.CommentThread): string {
|
|
89
89
|
const rawText = thread.comments[0]?.text ?? '';
|
|
90
|
+
|
|
90
91
|
const details: string[] = [];
|
|
91
92
|
|
|
93
|
+
if (thread.anchor.vePath) {
|
|
94
|
+
details.push(`- DevTools VEPath: ${thread.anchor.vePath}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
92
97
|
if (thread.anchor.textSignature) {
|
|
93
98
|
details.push(`- DevTools element: ${thread.anchor.textSignature}`);
|
|
94
99
|
}
|
|
@@ -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
|
/**
|
|
@@ -75,9 +76,8 @@ export async function getMarkdownFileContent(filename: string): Promise<string>
|
|
|
75
76
|
|
|
76
77
|
export async function createIssueDescriptionFromMarkdown(description: MarkdownIssueDescription):
|
|
77
78
|
Promise<IssueDescription> {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
return createIssueDescriptionFromRawMarkdown(rawMarkdownWithPlaceholdersReplaced, description);
|
|
79
|
+
const markdown = await getMarkdownFileContent(description.file);
|
|
80
|
+
return createIssueDescriptionFromRawMarkdown(markdown, description);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
@@ -85,7 +85,7 @@ export async function createIssueDescriptionFromMarkdown(description: MarkdownIs
|
|
|
85
85
|
*/
|
|
86
86
|
export function createIssueDescriptionFromRawMarkdown(
|
|
87
87
|
markdown: string, description: MarkdownIssueDescription): IssueDescription {
|
|
88
|
-
const markdownAst =
|
|
88
|
+
const markdownAst = TextUtils.Markdown.tokenizeWithPlaceholders(markdown, description.substitutions);
|
|
89
89
|
const markdownTitle = findTitleFromMarkdownAst(markdownAst);
|
|
90
90
|
if (!markdownTitle) {
|
|
91
91
|
throw new Error('Markdown issue descriptions must start with a heading');
|
|
@@ -99,49 +99,6 @@ export function createIssueDescriptionFromRawMarkdown(
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const validPlaceholderMatchPattern = /\{(PLACEHOLDER_[a-zA-Z][a-zA-Z0-9]*)\}/g;
|
|
103
|
-
const validPlaceholderNamePattern = /PLACEHOLDER_[a-zA-Z][a-zA-Z0-9]*/;
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Replaces placeholders in markdown text with a string provided by the
|
|
107
|
-
* `substitutions` map. To keep mental overhead to a minimum, the same
|
|
108
|
-
* syntax is used as for l10n placeholders. Please note that the
|
|
109
|
-
* placeholders require a mandatory 'PLACEHOLDER_' prefix.
|
|
110
|
-
*
|
|
111
|
-
* Example:
|
|
112
|
-
* const str = "This is markdown with `code` and two placeholders, namely {PLACEHOLDER_PH1} and {PLACEHOLDER_PH2}".
|
|
113
|
-
* const result = substitutePlaceholders(str, new Map([['PLACEHOLDER_PH1', 'foo'], ['PLACEHOLDER_PH2', 'bar']]));
|
|
114
|
-
*
|
|
115
|
-
* Exported only for unit testing.
|
|
116
|
-
*/
|
|
117
|
-
export function substitutePlaceholders(markdown: string, substitutions?: Map<string, string>): string {
|
|
118
|
-
const unusedPlaceholders = new Set(substitutions ? substitutions.keys() : []);
|
|
119
|
-
validatePlaceholders(unusedPlaceholders);
|
|
120
|
-
|
|
121
|
-
const result = markdown.replace(validPlaceholderMatchPattern, (_, placeholder) => {
|
|
122
|
-
const replacement = substitutions ? substitutions.get(placeholder) : undefined;
|
|
123
|
-
if (replacement === undefined) {
|
|
124
|
-
throw new Error(`No replacement provided for placeholder '${placeholder}'.`);
|
|
125
|
-
}
|
|
126
|
-
unusedPlaceholders.delete(placeholder);
|
|
127
|
-
return replacement;
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (unusedPlaceholders.size > 0) {
|
|
131
|
-
throw new Error(`Unused replacements provided: ${[...unusedPlaceholders]}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return result;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Ensure that all provided placeholders match the naming pattern. **/
|
|
138
|
-
function validatePlaceholders(placeholders: Set<string>): void {
|
|
139
|
-
const invalidPlaceholders = [...placeholders].filter(placeholder => !validPlaceholderNamePattern.test(placeholder));
|
|
140
|
-
if (invalidPlaceholders.length > 0) {
|
|
141
|
-
throw new Error(`Invalid placeholders provided in the substitutions map: ${invalidPlaceholders}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
102
|
export function findTitleFromMarkdownAst(markdownAst: Marked.Marked.Token[]): string|null {
|
|
146
103
|
if (markdownAst.length === 0 || markdownAst[0].type !== 'heading' || markdownAst[0].depth !== 1) {
|
|
147
104
|
return null;
|
|
@@ -64,7 +64,7 @@ export class FileSystem implements Workspace.Workspace.Project {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
displayName(): string {
|
|
67
|
-
const
|
|
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('\\');
|
|
@@ -4,16 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
6
6
|
import * as CommentManager from '../../models/comment_manager/comment_manager.js';
|
|
7
|
+
import * as Buttons from '../../ui/components/buttons/buttons.js';
|
|
7
8
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
8
9
|
import * as Lit from '../../ui/lit/lit.js';
|
|
9
10
|
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
|
|
10
11
|
|
|
12
|
+
import commentsStatusBarPillStyles from './commentsStatusBarPill.css.js';
|
|
13
|
+
|
|
11
14
|
const UIStrings = {
|
|
12
15
|
/**
|
|
13
16
|
* @description Button text for the comments status bar pill showing the number of open comments.
|
|
14
17
|
* @example {2} PH1
|
|
15
18
|
*/
|
|
16
19
|
commentsCount: 'Comments ({PH1})',
|
|
20
|
+
/**
|
|
21
|
+
* @description Button text for sending all draft comments to the agent in the comments status bar pill.
|
|
22
|
+
* @example {2} PH1
|
|
23
|
+
*/
|
|
24
|
+
sendToAgentCount: 'Send to agent ({PH1})',
|
|
17
25
|
} as const;
|
|
18
26
|
const str_ = i18n.i18n.registerUIStrings('panels/common/CommentsStatusBarPill.ts', UIStrings);
|
|
19
27
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
@@ -23,22 +31,38 @@ const {html, render} = Lit;
|
|
|
23
31
|
export interface ViewInput {
|
|
24
32
|
threads: CommentManager.CommentManager.CommentThread[];
|
|
25
33
|
onPillClick: () => void;
|
|
34
|
+
onSendToAgentClick?: () => void;
|
|
26
35
|
disabled?: boolean;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;
|
|
30
39
|
|
|
31
40
|
export const DEFAULT_VIEW: View = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
|
|
41
|
+
const unsentCount = input.threads.filter(thread => thread.status === 'ACTIVE').length;
|
|
42
|
+
const tooltip = input.threads.flatMap(thread => thread.comments.map(comment => comment.text)).join('\n');
|
|
32
43
|
// clang-format off
|
|
33
44
|
render(html`
|
|
45
|
+
<style>${commentsStatusBarPillStyles}</style>
|
|
34
46
|
${input.threads.length <= 0 ? Lit.nothing : html`
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
<div class="pill-container">
|
|
48
|
+
<button
|
|
49
|
+
class="devtools-pill"
|
|
50
|
+
title=${tooltip}
|
|
51
|
+
?disabled=${input.disabled}
|
|
52
|
+
@click=${input.onPillClick}
|
|
53
|
+
jslog=${VisualLogging.action('comments-status-bar-pill').track({click: true})}>
|
|
54
|
+
${i18nString(UIStrings.commentsCount, {PH1: input.threads.length})}
|
|
55
|
+
</button>
|
|
56
|
+
${unsentCount > 0 ? html`
|
|
57
|
+
<devtools-button
|
|
58
|
+
.variant=${Buttons.Button.Variant.PRIMARY}
|
|
59
|
+
.disabled=${Boolean(input.disabled)}
|
|
60
|
+
.jslogContext=${'comments-send-to-agent'}
|
|
61
|
+
@click=${input.onSendToAgentClick}>
|
|
62
|
+
${i18nString(UIStrings.sendToAgentCount, {PH1: unsentCount})}
|
|
63
|
+
</devtools-button>
|
|
64
|
+
` : Lit.nothing}
|
|
65
|
+
</div>
|
|
42
66
|
`}
|
|
43
67
|
`, target);
|
|
44
68
|
// clang-format on
|
|
@@ -100,9 +124,18 @@ export class CommentsStatusBarPill extends UI.Widget.Widget {
|
|
|
100
124
|
this.#commentManager.getCommentThreads().filter(thread => thread.status !== 'DRAFT') :
|
|
101
125
|
[],
|
|
102
126
|
onPillClick: this.#handlePillClick,
|
|
127
|
+
onSendToAgentClick: this.#handleSendToAgentClick,
|
|
103
128
|
};
|
|
104
129
|
this.#view(viewInput, undefined, this.contentElement);
|
|
105
130
|
}
|
|
106
131
|
|
|
107
132
|
#handlePillClick = (): void => {};
|
|
133
|
+
|
|
134
|
+
#handleSendToAgentClick = (): void => {
|
|
135
|
+
for (const thread of this.#commentManager.getCommentThreads()) {
|
|
136
|
+
if (thread.status === 'ACTIVE') {
|
|
137
|
+
thread.sendToAgent();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
108
141
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 The Chromium Authors
|
|
3
|
+
* Use of this source code is governed by a BSD-style license that can be
|
|
4
|
+
* found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
@scope to (devtools-widget > *) {
|
|
8
|
+
.pill-container {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: var(--sys-size-3);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -435,7 +435,11 @@ export class IssueView extends UI.TreeOutline.TreeElement {
|
|
|
435
435
|
messageElement.setCollapsible(false);
|
|
436
436
|
messageElement.selectable = false;
|
|
437
437
|
const markdownComponent = new MarkdownView.MarkdownView.MarkdownView();
|
|
438
|
-
markdownComponent.data = {
|
|
438
|
+
markdownComponent.data = {
|
|
439
|
+
tokens: this.#description.markdown,
|
|
440
|
+
renderer: new MarkdownView.MarkdownPlaceholderLitRenderer.MarkdownPlaceholderLitRenderer(
|
|
441
|
+
this.#description.substitutions),
|
|
442
|
+
};
|
|
439
443
|
messageElement.listItemElement.appendChild(markdownComponent);
|
|
440
444
|
UI.ARIAUtils.setPositionInSet(messageElement.listItemElement, 1);
|
|
441
445
|
UI.ARIAUtils.setSetSize(messageElement.listItemElement, this.#description.links.length === 0 ? 2 : 3);
|