chrome-devtools-frontend 1.0.1592129 → 1.0.1592362

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/front_end/core/host/UserMetrics.ts +2 -1
  2. package/front_end/core/protocol_client/InspectorBackend.ts +4 -0
  3. package/front_end/core/root/ExperimentNames.ts +1 -0
  4. package/front_end/core/sdk/EmulationModel.ts +41 -1
  5. package/front_end/entrypoints/main/MainImpl.ts +8 -0
  6. package/front_end/generated/InspectorBackendCommands.ts +1 -0
  7. package/front_end/generated/protocol-mapping.d.ts +6 -0
  8. package/front_end/generated/protocol-proxy-api.d.ts +7 -0
  9. package/front_end/generated/protocol.ts +16 -0
  10. package/front_end/models/ai_assistance/agents/BreakpointDebuggerAgent.ts +379 -25
  11. package/front_end/models/ai_assistance/agents/BreakpointDebuggerAgentOverlay.ts +87 -0
  12. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.snapshot.txt +8 -8
  13. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +65 -44
  14. package/front_end/models/computed_style/ComputedStyleModel.ts +40 -6
  15. package/front_end/models/emulation/DeviceModeModel.ts +47 -0
  16. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +11 -2
  17. package/front_end/panels/ai_assistance/components/MarkdownRendererWithCodeBlock.ts +55 -1
  18. package/front_end/panels/application/CookieItemsView.ts +194 -134
  19. package/front_end/panels/application/DeviceBoundSessionsTreeElement.ts +1 -1
  20. package/front_end/panels/application/StorageItemsToolbar.ts +25 -2
  21. package/front_end/panels/application/cookieItemsView.css +28 -26
  22. package/front_end/panels/console/ConsoleViewMessage.ts +1 -1
  23. package/front_end/panels/elements/ComputedStyleWidget.ts +11 -25
  24. package/front_end/panels/elements/ElementsPanel.ts +26 -3
  25. package/front_end/panels/elements/StylePropertyTreeElement.ts +33 -0
  26. package/front_end/panels/elements/StylesAiCodeCompletionProvider.ts +148 -1
  27. package/front_end/panels/elements/elements-meta.ts +3 -5
  28. package/front_end/panels/elements/stylePropertiesTreeOutline.css +6 -0
  29. package/front_end/panels/emulation/DeviceModeToolbar.ts +25 -0
  30. package/front_end/panels/timeline/components/metricCard.css +0 -4
  31. package/front_end/ui/components/text_editor/AiCodeCompletionProvider.ts +13 -2
  32. package/front_end/ui/legacy/components/cookie_table/CookiesTable.ts +44 -24
  33. package/front_end/ui/visual_logging/Debugging.ts +4 -0
  34. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  35. package/package.json +1 -1
@@ -0,0 +1,87 @@
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
+ /**
6
+ * This file contains the overlay script that is injected into the page when the
7
+ * BreakpointDebuggerAgent is waiting for a user action.
8
+ *
9
+ * This is a temporary solution for the prototype. In the long term, we should
10
+ * use a proper overlay or a different mechanism to communicate with the user,
11
+ * rather than injecting a script into the page. This approach is fine for the
12
+ * prototype but should be replaced before production.
13
+ */
14
+
15
+ import * as SDK from '../../../core/sdk/sdk.js';
16
+
17
+ export async function injectOverlay(): Promise<void> {
18
+ const targetManager = SDK.TargetManager.TargetManager.instance();
19
+ const primaryTarget = targetManager.primaryPageTarget();
20
+
21
+ await primaryTarget?.runtimeAgent().invoke_evaluate({
22
+ expression: WAIT_FOR_USER_ACTION_OVERLAY_SCRIPT,
23
+ });
24
+ }
25
+
26
+ export async function removeOverlay(): Promise<void> {
27
+ const targetManager = SDK.TargetManager.TargetManager.instance();
28
+ const primaryTarget = targetManager.primaryPageTarget();
29
+
30
+ await primaryTarget?.runtimeAgent().invoke_evaluate({
31
+ expression: REMOVE_OVERLAY_SCRIPT,
32
+ });
33
+ }
34
+
35
+ const WAIT_FOR_USER_ACTION_OVERLAY_SCRIPT = `
36
+ (function() {
37
+ const devtoolsOverlayId = 'devtools-waiting-overlay';
38
+ let overlay = document.getElementById(devtoolsOverlayId);
39
+ if (!overlay) {
40
+ overlay = document.createElement('div');
41
+ overlay.id = devtoolsOverlayId;
42
+ overlay.style.position = 'fixed';
43
+ overlay.style.top = '0';
44
+ overlay.style.left = '0';
45
+ overlay.style.width = '100vw';
46
+ overlay.style.height = '100vh';
47
+ overlay.style.pointerEvents = 'none';
48
+ overlay.style.zIndex = '2147483647';
49
+ overlay.style.boxSizing = 'border-box';
50
+ overlay.style.border = '10px solid red';
51
+ overlay.style.animation = 'devtools-fade 1.5s infinite alternate';
52
+ const text = document.createElement('div');
53
+ text.innerText = 'Trigger the breakpoint again';
54
+ text.style.position = 'absolute';
55
+ text.style.top = '10px';
56
+ text.style.left = '50%';
57
+ text.style.transform = 'translateX(-50%)';
58
+ text.style.backgroundColor = 'red';
59
+ text.style.color = 'white';
60
+ text.style.padding = '10px 20px';
61
+ text.style.borderRadius = '5px';
62
+ text.style.fontFamily = 'system-ui, sans-serif';
63
+ text.style.fontSize = '16px';
64
+ text.style.fontWeight = 'bold';
65
+ text.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
66
+ overlay.appendChild(text);
67
+
68
+ const style = document.createElement('style');
69
+ style.id = devtoolsOverlayId + '-style';
70
+ style.innerText = '@keyframes devtools-fade { from { opacity: 0.5; } to { opacity: 1; } }';
71
+ // Head might not exist immediately on a completely blank page, fallback to documentElement
72
+ (document.head || document.documentElement).appendChild(style);
73
+
74
+ document.documentElement.appendChild(overlay);
75
+ }
76
+ })();
77
+ `;
78
+
79
+ const REMOVE_OVERLAY_SCRIPT = `
80
+ (function() {
81
+ const devtoolsOverlayId = 'devtools-waiting-overlay';
82
+ const overlay = document.getElementById(devtoolsOverlayId);
83
+ if (overlay) overlay.remove();
84
+ const style = document.getElementById(devtoolsOverlayId + '-style');
85
+ if (style) style.remove();
86
+ })();
87
+ `;
@@ -48,12 +48,12 @@ Content:
48
48
  "description": "",
49
49
  "nullable": true,
50
50
  "required": [
51
- "url"
51
+ "id"
52
52
  ],
53
53
  "properties": {
54
- "url": {
54
+ "id": {
55
55
  "type": 1,
56
- "description": "The url of the requests",
56
+ "description": "The id of the network request",
57
57
  "nullable": false
58
58
  }
59
59
  }
@@ -72,18 +72,18 @@ Content:
72
72
  },
73
73
  {
74
74
  "name": "selectSourceFile",
75
- "description": "Selects a source file. Use this when asked about files on the page. Use listSourceFiles if you don't know the full path name.",
75
+ "description": "Selects a source file. Use this when asked about files on the page. Use listSourceFiles to find the file ID.",
76
76
  "parameters": {
77
77
  "type": 6,
78
78
  "description": "",
79
79
  "nullable": true,
80
80
  "required": [
81
- "name"
81
+ "id"
82
82
  ],
83
83
  "properties": {
84
- "name": {
85
- "type": 1,
86
- "description": "The full path name of the file you want to select.",
84
+ "id": {
85
+ "type": 3,
86
+ "description": "The id (URL) of the file you want to select.",
87
87
  "nullable": false
88
88
  }
89
89
  }
@@ -5,7 +5,6 @@
5
5
  import * as Common from '../../../core/common/common.js';
6
6
  import * as Host from '../../../core/host/host.js';
7
7
  import * as i18n from '../../../core/i18n/i18n.js';
8
- import * as Platform from '../../../core/platform/platform.js';
9
8
  import * as Root from '../../../core/root/root.js';
10
9
  import * as SDK from '../../../core/sdk/sdk.js';
11
10
  import * as Logs from '../../logs/logs.js';
@@ -35,13 +34,12 @@ You are a Web Development Assistant integrated into Chrome DevTools. Your tone i
35
34
  You aim to help developers of all levels, prioritizing teaching web concepts as the primary entry point for any solution.
36
35
 
37
36
  # Considerations
38
- * Determine what the question the domain of the question is - styling, network, sources, performance or other part of DevTools.
37
+ * Determine what is the domain of the question - styling, network, sources, performance or other part of DevTools.
39
38
  * Proactively try to gather additional data. If a select specific data can be selected, select one.
40
39
  * Always try select single specific context before answering the question.
41
40
  * Avoid making assumptions without sufficient evidence, and always seek further clarification if needed.
42
41
  * When presenting solutions, clearly distinguish between the primary cause and contributing factors.
43
42
  * Please answer only if you are sure about the answer. Otherwise, explain why you're not able to answer.
44
- * When answering, always consider MULTIPLE possible solutions.
45
43
  * If you are unable to gather more information provide a comprehensive guide to how to fix the issue using Chrome DevTools and explain how and why.
46
44
  * You can suggest any panel or flow in Chrome DevTools that may help the user out
47
45
 
@@ -50,9 +48,13 @@ You aim to help developers of all levels, prioritizing teaching web concepts as
50
48
  * Always specify the language for code blocks (e.g., \`\`\`css, \`\`\`javascript).
51
49
  * Keep text responses concise and scannable.
52
50
 
51
+ * **CRITICAL** If a tool returns an empty list, immediately pivot to the next logical tool (e.g., from sources to network).
52
+ * **CRITICAL** Always exhaust all possible way to find and select context from different domains.
53
53
  * **CRITICAL** NEVER write full Python programs - you should only write individual statements that invoke a single function from the provided library.
54
54
  * **CRITICAL** NEVER output text before a function call. Always do a function call first.
55
55
  * **CRITICAL** You are a debugging assistant in DevTools. NEVER provide answers to questions of unrelated topics such as legal advice, financial advice, personal opinions, medical advice, religion, race, politics, sexuality, gender, or any other non web-development topics. Answer "Sorry, I can't answer that. I'm best at questions about debugging web pages." to such questions.
56
+ * **CRITICAL** When referring to DevTools resource output a markdown link to the object using the format \`[<text>](#<type>-<ID>)\`.
57
+ * The only available types are \`#req\` for network request and \`#file\` for source files. Only use ID inside the link, never ask about user selecting by ID.
56
58
  `;
57
59
 
58
60
  /**
@@ -117,6 +119,7 @@ export class ContextSelectionAgent extends AiAgent<never> {
117
119
  }
118
120
 
119
121
  requests.push({
122
+ id: request.requestId(),
120
123
  url: request.url(),
121
124
  statusCode: request.statusCode,
122
125
  duration: i18n.TimeUtilities.secondsToString(request.duration),
@@ -135,18 +138,18 @@ export class ContextSelectionAgent extends AiAgent<never> {
135
138
  },
136
139
  });
137
140
 
138
- this.declareFunction<{url: string}>('selectNetworkRequest', {
141
+ this.declareFunction<{id: string}>('selectNetworkRequest', {
139
142
  description:
140
143
  `Selects a specific network request to further provide information about. Use this when asked about network requests issues.`,
141
144
  parameters: {
142
145
  type: Host.AidaClient.ParametersTypes.OBJECT,
143
146
  description: '',
144
147
  nullable: true,
145
- required: ['url'],
148
+ required: ['id'],
146
149
  properties: {
147
- url: {
150
+ id: {
148
151
  type: Host.AidaClient.ParametersTypes.STRING,
149
- description: 'The url of the requests',
152
+ description: 'The id of the network request',
150
153
  nullable: false,
151
154
  },
152
155
  },
@@ -154,15 +157,12 @@ export class ContextSelectionAgent extends AiAgent<never> {
154
157
  displayInfoFromArgs: args => {
155
158
  return {
156
159
  title: lockedString('Getting network request…'),
157
- action: `selectNetworkRequest(${args.url})`,
160
+ action: `selectNetworkRequest(${args.id})`,
158
161
  };
159
162
  },
160
- handler: async ({url}) => {
161
- // TODO: Switch to using IDs to make is easier to link to as well.
163
+ handler: async ({id}) => {
162
164
  const request = Logs.NetworkLog.NetworkLog.instance().requests().find(req => {
163
- return req.url() === Platform.DevToolsPath.urlString`${url}` ||
164
- req.url() === Platform.DevToolsPath.urlString`${url.slice(0, -1)}` ||
165
- req.url() === Platform.DevToolsPath.urlString`${url}/`;
165
+ return req.requestId() === id;
166
166
  });
167
167
 
168
168
  if (request) {
@@ -195,32 +195,32 @@ export class ContextSelectionAgent extends AiAgent<never> {
195
195
  };
196
196
  },
197
197
  handler: async () => {
198
- // We get multiple of the same file name
199
- // so we create a set and the pick based
200
- // on heuristics in the select function
201
- const files = new Set();
202
- for (const file of this.#getUISourceCodes()) {
203
- files.add(file.fullDisplayName());
198
+ const files: Array<{file: string, id: number | undefined}> = [];
199
+ for (const file of ContextSelectionAgent.getUISourceCodes()) {
200
+ files.push({
201
+ file: file.fullDisplayName(),
202
+ id: ContextSelectionAgent.uiSourceCodeId.get(file),
203
+ });
204
204
  }
205
205
 
206
206
  return {
207
- result: [...files],
207
+ result: files,
208
208
  };
209
209
  },
210
210
  });
211
211
 
212
- this.declareFunction<{name: string}>('selectSourceFile', {
212
+ this.declareFunction<{id: number}>('selectSourceFile', {
213
213
  description:
214
- `Selects a source file. Use this when asked about files on the page. Use listSourceFiles if you don't know the full path name.`,
214
+ `Selects a source file. Use this when asked about files on the page. Use listSourceFiles to find the file ID.`,
215
215
  parameters: {
216
216
  type: Host.AidaClient.ParametersTypes.OBJECT,
217
217
  description: '',
218
218
  nullable: true,
219
- required: ['name'],
219
+ required: ['id'],
220
220
  properties: {
221
- name: {
222
- type: Host.AidaClient.ParametersTypes.STRING,
223
- description: 'The full path name of the file you want to select.',
221
+ id: {
222
+ type: Host.AidaClient.ParametersTypes.INTEGER,
223
+ description: 'The id (URL) of the file you want to select.',
224
224
  nullable: false,
225
225
  },
226
226
  },
@@ -228,22 +228,19 @@ export class ContextSelectionAgent extends AiAgent<never> {
228
228
  displayInfoFromArgs: args => {
229
229
  return {
230
230
  title: lockedString('Getting source file…'),
231
- action: `selectSourceFile(${args.name})`,
231
+ action: `selectSourceFile(${args.id})`,
232
232
  };
233
233
  },
234
234
  handler: async params => {
235
- // In some cases we get multiple files
236
- // use the heuristics bellow to pick the better one
237
- const files = this.#getUISourceCodes().filter(file => file.fullDisplayName() === params.name);
235
+ const file = ContextSelectionAgent.getUISourceCodes().find(
236
+ file => ContextSelectionAgent.uiSourceCodeId.get(file) === params.id);
238
237
 
239
- if (files.length === 0) {
238
+ if (!file) {
240
239
  return {
241
240
  error: 'Unable to find file.',
242
241
  };
243
242
  }
244
243
 
245
- // This help us pick the file that is resolved source map.
246
- const file = files.find(f => f.contentType().isFromSourceMap()) ?? files[0];
247
244
  return {
248
245
  context: new FileContext(file),
249
246
  description: 'User selected a source file',
@@ -315,27 +312,51 @@ export class ContextSelectionAgent extends AiAgent<never> {
315
312
  });
316
313
  }
317
314
 
318
- #getUISourceCodes = (): Workspace.UISourceCode.UISourceCode[] => {
315
+ async * handleContextDetails(): AsyncGenerator<ContextResponse, void, void> {
316
+ }
317
+
318
+ override async enhanceQuery(query: string): Promise<string> {
319
+ return query;
320
+ }
321
+
322
+ static lastSourceId = 0;
323
+ static uiSourceCodeId = new WeakMap<Workspace.UISourceCode.UISourceCode, number>();
324
+ /**
325
+ * This is a heuristic algorithm that gets all the source files coming from the
326
+ * network and assigns unique ids to be linked from the LLM Markdown response.
327
+ * Steps we do:
328
+ * 1. Get all project that are coming from the Network. This scopes down
329
+ * sources exposed to the LLM
330
+ * 2. Remove all ignore listed source code. We further reduce thing that the
331
+ * user most likely does not have interest in, from global setting.
332
+ * 3.1. Source files don't have an uniqueId so we use the URL to differentiate
333
+ * them.
334
+ * 3.2. In cases where we encounter a duplicated URLs we prefer the latest one
335
+ * coming from SourceMaps (usually only one) as that has simple code and
336
+ * usually is what the user authored.
337
+ */
338
+ static getUISourceCodes(): Workspace.UISourceCode.UISourceCode[] {
319
339
  const workspace = Workspace.Workspace.WorkspaceImpl.instance();
320
340
  const projects =
321
341
  workspace.projects().filter(project => project.type() === Workspace.Workspace.projectTypes.Network);
322
- const uiSourceCodes = [];
342
+ const uiSourceCodes = new Map<string, Workspace.UISourceCode.UISourceCode>();
343
+
323
344
  for (const project of projects) {
324
345
  for (const uiSourceCode of project.uiSourceCodes()) {
325
346
  if (uiSourceCode.isIgnoreListed()) {
326
347
  continue;
327
348
  }
328
- uiSourceCodes.push(uiSourceCode);
349
+ const url = uiSourceCode.url();
350
+ // This helps us pick the file that is a resolved source map.
351
+ if (!uiSourceCodes.get(url) || uiSourceCode.contentType().isFromSourceMap()) {
352
+ uiSourceCodes.set(url, uiSourceCode);
353
+ if (!ContextSelectionAgent.uiSourceCodeId.has(uiSourceCode)) {
354
+ ContextSelectionAgent.uiSourceCodeId.set(uiSourceCode, ++ContextSelectionAgent.lastSourceId);
355
+ }
356
+ }
329
357
  }
330
358
  }
331
359
 
332
- return uiSourceCodes;
333
- };
334
-
335
- async * handleContextDetails(): AsyncGenerator<ContextResponse, void, void> {
336
- }
337
-
338
- override async enhanceQuery(query: string): Promise<string> {
339
- return query;
360
+ return [...uiSourceCodes.values()];
340
361
  }
341
362
  }
@@ -12,17 +12,18 @@ import * as SDK from '../../core/sdk/sdk.js';
12
12
  * Model trackComputedStyleUpdatesForNode method.
13
13
  */
14
14
  export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
15
- #node: SDK.DOMModel.DOMNode|null;
16
- #cssModel: SDK.CSSModel.CSSModel|null;
17
- private eventListeners: Common.EventTarget.EventDescriptor[];
15
+ #node: SDK.DOMModel.DOMNode|null = null;
16
+ #cssModel: SDK.CSSModel.CSSModel|null = null;
17
+ private eventListeners: Common.EventTarget.EventDescriptor[] = [];
18
18
  private frameResizedTimer?: number;
19
19
  private computedStylePromise?: Promise<ComputedStyle|null>;
20
20
 
21
21
  constructor(node?: SDK.DOMModel.DOMNode|null) {
22
22
  super();
23
- this.#cssModel = null;
24
- this.eventListeners = [];
25
- this.#node = node ?? null;
23
+ if (node) {
24
+ // Call the explicit setter to trigger the setup and event binding.
25
+ this.node = node;
26
+ }
26
27
  }
27
28
 
28
29
  get node(): SDK.DOMModel.DOMNode|null {
@@ -39,6 +40,21 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
39
40
  return this.#cssModel?.isEnabled() ? this.#cssModel : null;
40
41
  }
41
42
 
43
+ /**
44
+ * Clears all event listeners to ensure the instance can be GC'd without leaking memory.
45
+ */
46
+ dispose(): void {
47
+ Common.EventTarget.removeEventListeners(this.eventListeners);
48
+ this.eventListeners = [];
49
+ this.node = null;
50
+ this.#cssModel = null;
51
+ this.computedStylePromise = undefined;
52
+ if (this.frameResizedTimer) {
53
+ clearTimeout(this.frameResizedTimer);
54
+ this.frameResizedTimer = undefined;
55
+ }
56
+ }
57
+
42
58
  private updateModel(cssModel: SDK.CSSModel.CSSModel|null): void {
43
59
  if (this.#cssModel === cssModel) {
44
60
  return;
@@ -168,6 +184,24 @@ export class ComputedStyleModel extends Common.ObjectWrapper.ObjectWrapper<Event
168
184
  }
169
185
  return matchedStyles.node() === this.node ? matchedStyles : null;
170
186
  }
187
+
188
+ computePropertyTraces(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles):
189
+ Map<string, SDK.CSSProperty.CSSProperty[]> {
190
+ const result = new Map<string, SDK.CSSProperty.CSSProperty[]>();
191
+ for (const style of matchedStyles.nodeStyles()) {
192
+ const allProperties = style.allProperties();
193
+ for (const property of allProperties) {
194
+ if (!property.activeInStyle() || !matchedStyles.propertyState(property)) {
195
+ continue;
196
+ }
197
+
198
+ const matches = result.get(property.name) ?? [];
199
+ matches.push(property);
200
+ result.set(property.name, matches);
201
+ }
202
+ }
203
+ return result;
204
+ }
171
205
  }
172
206
 
173
207
  export const enum Events {
@@ -113,6 +113,7 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
113
113
  #emulationModel: SDK.EmulationModel.EmulationModel|null;
114
114
  #onModelAvailable: (() => void)|null;
115
115
  #outlineRect?: Rect;
116
+ #screenOrientationLocked: boolean;
116
117
 
117
118
  private constructor() {
118
119
  super();
@@ -172,6 +173,7 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
172
173
 
173
174
  this.#emulationModel = null;
174
175
  this.#onModelAvailable = null;
176
+ this.#screenOrientationLocked = false;
175
177
  SDK.TargetManager.TargetManager.instance().observeModels(SDK.EmulationModel.EmulationModel, this);
176
178
  }
177
179
 
@@ -442,6 +444,9 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
442
444
  this.#onModelAvailable = null;
443
445
  callback();
444
446
  }
447
+ emulationModel.addEventListener(
448
+ SDK.EmulationModel.EmulationModelEvents.SCREEN_ORIENTATION_LOCK_CHANGED, this.onScreenOrientationLockChanged,
449
+ this);
445
450
  const resourceTreeModel = emulationModel.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
446
451
  if (resourceTreeModel) {
447
452
  resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.FrameResized, this.onFrameChange, this);
@@ -454,7 +459,12 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
454
459
 
455
460
  modelRemoved(emulationModel: SDK.EmulationModel.EmulationModel): void {
456
461
  if (this.#emulationModel === emulationModel) {
462
+ emulationModel.removeEventListener(
463
+ SDK.EmulationModel.EmulationModelEvents.SCREEN_ORIENTATION_LOCK_CHANGED, this.onScreenOrientationLockChanged,
464
+ this);
457
465
  this.#emulationModel = null;
466
+ this.#screenOrientationLocked = false;
467
+ this.dispatchEventToListeners(Events.UPDATED);
458
468
  }
459
469
  }
460
470
 
@@ -471,6 +481,43 @@ export class DeviceModeModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
471
481
  this.showHingeIfApplicable(overlayModel);
472
482
  }
473
483
 
484
+ private onScreenOrientationLockChanged(
485
+ event: Common.EventTarget.EventTargetEvent<SDK.EmulationModel.ScreenOrientationLockChangedEvent>): void {
486
+ this.#screenOrientationLocked = event.data.locked;
487
+ if (event.data.locked && event.data.orientation) {
488
+ this.applyOrientationLock(event.data.orientation);
489
+ }
490
+ this.dispatchEventToListeners(Events.UPDATED);
491
+ }
492
+
493
+ private applyOrientationLock(orientation: Protocol.Emulation.ScreenOrientation): void {
494
+ const wantsLandscape = orientation.type === Protocol.Emulation.ScreenOrientationType.LandscapePrimary ||
495
+ orientation.type === Protocol.Emulation.ScreenOrientationType.LandscapeSecondary;
496
+
497
+ if (this.#type === Type.Device && this.#device && this.#mode) {
498
+ // For device emulation, switch to the matching orientation mode.
499
+ const isCurrentlyLandscape =
500
+ this.#mode.orientation === Horizontal || this.#mode.orientation === HorizontalSpanned;
501
+ if (wantsLandscape !== isCurrentlyLandscape) {
502
+ const rotationPartner = this.#device.getRotationPartner(this.#mode);
503
+ if (rotationPartner) {
504
+ this.emulate(this.#type, this.#device, rotationPartner);
505
+ }
506
+ }
507
+ } else if (this.#type === Type.Responsive) {
508
+ // For responsive mode, swap width/height if orientation doesn't match.
509
+ const appliedSize = this.appliedDeviceSize();
510
+ const isCurrentlyLandscape = appliedSize.width > appliedSize.height;
511
+ if (wantsLandscape !== isCurrentlyLandscape) {
512
+ this.setSizeAndScaleToFit(appliedSize.height, appliedSize.width);
513
+ }
514
+ }
515
+ }
516
+
517
+ isScreenOrientationLocked(): boolean {
518
+ return this.#screenOrientationLocked;
519
+ }
520
+
474
521
  private scaleSettingChanged(): void {
475
522
  this.calculateAndEmulate(false);
476
523
  }
@@ -13,6 +13,7 @@ import * as SDK from '../../core/sdk/sdk.js';
13
13
  import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js';
14
14
  import * as Annotations from '../../models/annotations/annotations.js';
15
15
  import * as Badges from '../../models/badges/badges.js';
16
+ import * as Greendev from '../../models/greendev/greendev.js';
16
17
  import * as TextUtils from '../../models/text_utils/text_utils.js';
17
18
  import type * as Trace from '../../models/trace/trace.js';
18
19
  import * as Workspace from '../../models/workspace/workspace.js';
@@ -461,7 +462,8 @@ function defaultView(input: ViewInput, output: PanelViewOutput, target: HTMLElem
461
462
 
462
463
  const shouldShowWalkthrough = input.state === ViewState.CHAT_VIEW && input.walkthrough.isExpanded;
463
464
 
464
- if (Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled) {
465
+ if (Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled ||
466
+ Greendev.Prototypes.instance().isEnabled('breakpointDebuggerAgent')) {
465
467
  Lit.render(html`
466
468
  ${toolbarView(input)}
467
469
  <div class="ai-assistance-view-container">
@@ -915,7 +917,7 @@ export class AiAssistancePanel extends UI.Panel.Panel {
915
917
  this.requestUpdate();
916
918
  }
917
919
 
918
- async handleBreakpointConversation(uiLocation: Workspace.UISourceCode.UILocation): Promise<void> {
920
+ async handleBreakpointConversation(uiLocation: Workspace.UISourceCode.UILocation, errorMsg?: string): Promise<void> {
919
921
  const context = new AiAssistanceModel.BreakpointDebuggerAgent.BreakpointContext(uiLocation);
920
922
  this.#selectedBreakpoint = context;
921
923
  const conversation = new AiAssistanceModel.AiConversation.AiConversation(
@@ -934,6 +936,9 @@ export class AiAssistancePanel extends UI.Panel.Panel {
934
936
  this.#conversation?.setContext(context);
935
937
  this.requestUpdate();
936
938
  await UI.ViewManager.ViewManager.instance().showView(AiAssistancePanel.panelName);
939
+ const prompt = errorMsg ? `debug the error "${errorMsg}" using breakpoint debugging agent` :
940
+ 'debug the error using breakpoint debugging agent';
941
+ await this.#startConversation(prompt);
937
942
  }
938
943
 
939
944
  override wasShown(): void {
@@ -1658,6 +1663,10 @@ export class AiAssistancePanel extends UI.Panel.Panel {
1658
1663
  parts: [],
1659
1664
  };
1660
1665
  this.#messages.push(systemMessage);
1666
+ if (Greendev.Prototypes.instance().isEnabled('breakpointDebuggerAgent') &&
1667
+ this.#conversation?.type === AiAssistanceModel.AiHistoryStorage.ConversationType.BREAKPOINT) {
1668
+ this.#openWalkthrough(systemMessage);
1669
+ }
1661
1670
  break;
1662
1671
  }
1663
1672
  case AiAssistanceModel.AiAgent.ResponseType.QUERYING: {
@@ -2,9 +2,15 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ import * as Common from '../../../core/common/common.js';
6
+ import * as Platform from '../../../core/platform/platform.js';
7
+ import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
8
+ import * as Logs from '../../../models/logs/logs.js';
5
9
  import type * as Marked from '../../../third_party/marked/marked.js';
6
10
  import * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
7
- import type * as Lit from '../../../ui/lit/lit.js';
11
+ import * as Lit from '../../../ui/lit/lit.js';
12
+
13
+ const {html} = Lit;
8
14
 
9
15
  /**
10
16
  * The model returns multiline code blocks in an erroneous way with the language being in new line.
@@ -19,7 +25,43 @@ import type * as Lit from '../../../ui/lit/lit.js';
19
25
  * ```
20
26
  **/
21
27
  export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.MarkdownInsightRenderer {
28
+ #revealableLink(revealable: unknown, label: string): Lit.LitTemplate {
29
+ return html`<devtools-link @click=${(e: Event) => {
30
+ e.preventDefault();
31
+ e.stopPropagation();
32
+ void Common.Revealer.reveal(revealable);
33
+ }}>${Platform.StringUtilities.trimEndWithMaxLength(label, 100)}</devtools-link>`;
34
+ }
35
+
36
+ #renderLink(href: string): Lit.LitTemplate|null {
37
+ if (href.startsWith('#req-')) {
38
+ const request =
39
+ Logs.NetworkLog.NetworkLog.instance().requests().find(req => req.requestId() === href.substring(5));
40
+
41
+ if (request) {
42
+ return this.#revealableLink(request, request.url());
43
+ }
44
+
45
+ } else if (href.startsWith('#file-')) {
46
+ const file = AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.getUISourceCodes().find(
47
+ file => AiAssistanceModel.ContextSelectionAgent.ContextSelectionAgent.uiSourceCodeId.get(file) ===
48
+ Number(href.substring(6)));
49
+
50
+ if (file) {
51
+ return this.#revealableLink(file, file.name());
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
22
57
  override templateForToken(token: Marked.Marked.MarkedToken): Lit.LitTemplate|null {
58
+ if (token.type === 'link') {
59
+ const link = this.#renderLink(token.href);
60
+ if (link) {
61
+ return link;
62
+ }
63
+ }
64
+
23
65
  if (token.type === 'code') {
24
66
  const lines = (token.text).split('\n');
25
67
  if (lines[0]?.trim() === 'css') {
@@ -28,6 +70,18 @@ export class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.Mar
28
70
  }
29
71
  }
30
72
 
73
+ if (token.type === 'codespan') {
74
+ // LLM likes outputting the link inside a codespan block.
75
+ // Remove the codespan and render the link directly
76
+ const matches = token.text.match(/^\[.*\]\((.+)\)$/);
77
+ if (matches?.[1]) {
78
+ const link = this.#renderLink(matches[1]);
79
+ if (link) {
80
+ return link;
81
+ }
82
+ }
83
+ }
84
+
31
85
  return super.templateForToken(token);
32
86
  }
33
87
  }