chrome-devtools-frontend 1.0.1618066 → 1.0.1621064

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 (54) hide show
  1. package/docs/checklist/README.md +8 -7
  2. package/eslint.config.mjs +7 -0
  3. package/front_end/core/sdk/NetworkManager.ts +23 -2
  4. package/front_end/core/sdk/ServerSentEventsProtocol.ts +1 -1
  5. package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +119 -13
  6. package/front_end/generated/InspectorBackendCommands.ts +6 -6
  7. package/front_end/generated/SupportedCSSProperties.js +100 -100
  8. package/front_end/generated/protocol-mapping.d.ts +1 -9
  9. package/front_end/generated/protocol-proxy-api.d.ts +1 -9
  10. package/front_end/generated/protocol.ts +5 -0
  11. package/front_end/models/ai_assistance/agents/GreenDevAgent.ts +512 -0
  12. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +65 -85
  13. package/front_end/models/ai_assistance/agents/StylingAgent.ts +9 -16
  14. package/front_end/models/ai_assistance/ai_assistance.ts +2 -0
  15. package/front_end/models/bindings/DebuggerWorkspaceBinding.ts +21 -4
  16. package/front_end/models/bindings/SymbolizedError.ts +69 -2
  17. package/front_end/models/javascript_metadata/NativeFunctions.js +2 -2
  18. package/front_end/models/stack_trace/DetailedErrorStackParser.ts +11 -3
  19. package/front_end/models/stack_trace/ErrorStackParser.ts +18 -0
  20. package/front_end/panels/ai_assistance/components/ChatMessage.ts +84 -2
  21. package/front_end/panels/ai_assistance/components/WalkthroughUtils.ts +1 -1
  22. package/front_end/panels/ai_assistance/components/WalkthroughView.ts +9 -2
  23. package/front_end/panels/application/WebMCPView.ts +212 -89
  24. package/front_end/panels/application/webMCPView.css +260 -199
  25. package/front_end/panels/browser_debugger/CategorizedBreakpointsSidebarPane.ts +9 -5
  26. package/front_end/panels/changes/ChangesSidebar.ts +2 -2
  27. package/front_end/panels/changes/ChangesView.ts +4 -7
  28. package/front_end/panels/console/ConsoleViewMessage.ts +1 -19
  29. package/front_end/panels/coverage/CoverageView.ts +4 -5
  30. package/front_end/panels/elements/ElementsPanel.ts +9 -12
  31. package/front_end/panels/elements/StylePropertiesSection.ts +1 -1
  32. package/front_end/panels/elements/components/ElementsBreadcrumbs.ts +19 -20
  33. package/front_end/panels/elements/components/ElementsBreadcrumbsUtils.ts +29 -29
  34. package/front_end/panels/elements/components/QueryContainer.ts +5 -6
  35. package/front_end/panels/elements/components/components.ts +0 -2
  36. package/front_end/panels/emulation/MediaQueryInspector.ts +4 -7
  37. package/front_end/panels/mobile_throttling/NetworkThrottlingSelector.ts +47 -66
  38. package/front_end/panels/network/RequestConditionsDrawer.ts +6 -4
  39. package/front_end/panels/performance_monitor/PerformanceMonitor.ts +6 -9
  40. package/front_end/panels/protocol_monitor/JSONEditor.ts +2 -2
  41. package/front_end/panels/search/SearchView.ts +4 -7
  42. package/front_end/panels/sources/DebuggerPausedMessage.ts +18 -22
  43. package/front_end/panels/sources/ThreadsSidebarPane.ts +2 -4
  44. package/front_end/panels/web_audio/WebAudioView.ts +5 -4
  45. package/front_end/third_party/chromium/README.chromium +1 -1
  46. package/front_end/ui/components/icon_button/iconButton.css +1 -0
  47. package/front_end/ui/components/lists/list.css +4 -0
  48. package/front_end/ui/legacy/ViewRegistration.ts +2 -2
  49. package/front_end/ui/legacy/Widget.ts +9 -2
  50. package/front_end/ui/legacy/components/utils/Linkifier.ts +32 -11
  51. package/front_end/ui/visual_logging/KnownContextValues.ts +17 -0
  52. package/package.json +1 -1
  53. package/front_end/Images/src/dots-circle.svg +0 -10
  54. package/front_end/panels/elements/components/Helper.ts +0 -35
@@ -272,6 +272,12 @@ export interface StepPart {
272
272
  step: Step;
273
273
  }
274
274
 
275
+ /**
276
+ * Represents a part of the message that consists of one or more widgets.
277
+ * The agent can yield widgets directly as part of its response, separate
278
+ * from those returned by a specific tool call (which are encapsulated
279
+ * within a StepPart).
280
+ */
275
281
  export interface WidgetPart {
276
282
  type: 'widget';
277
283
  widgets: AiWidget[];
@@ -1111,8 +1117,82 @@ async function makeDomTreeWidget(widgetData: DomTreeAiWidget): Promise<WidgetMak
1111
1117
  *
1112
1118
  * This allows for a flexible and extensible system where new widget types
1113
1119
  * can be added to the AI responses and rendered in DevTools by adding
1114
- * corresponding \`make...Widget\` functions and handling them here.
1120
+ * corresponding `make...Widget` functions and handling them here.
1121
+ */
1122
+ /**
1123
+ * Generates a deterministic unique identifier for a given AiWidget based on
1124
+ * its name and identifying data. This signature is used for widget deduplication.
1115
1125
  */
1126
+ export function getWidgetSignature(widget: AiWidget): string {
1127
+ switch (widget.name) {
1128
+ case 'COMPUTED_STYLES':
1129
+ return `${widget.name}:${widget.data.backendNodeId}`;
1130
+ case 'CORE_VITALS':
1131
+ return `${widget.name}:${widget.data.insightSetKey}`;
1132
+ case 'STYLE_PROPERTIES':
1133
+ return `${widget.name}:${widget.data.backendNodeId}:${widget.data.selector ?? ''}`;
1134
+ case 'DOM_TREE':
1135
+ return `${widget.name}:${widget.data.root.backendNodeId()}`;
1136
+ case 'PERFORMANCE_TRACE':
1137
+ return `${widget.name}`;
1138
+ case 'PERF_INSIGHT':
1139
+ return `${widget.name}:${widget.data.insight}:${widget.data.insightData.insightKey}:${
1140
+ widget.data.insightData.navigation?.args?.data?.navigationId ?? 'no-nav-id'}`;
1141
+ case 'TIMELINE_RANGE_SUMMARY':
1142
+ return `${widget.name}:${widget.data.track}:${widget.data.bounds.min}-${widget.data.bounds.max}`;
1143
+ case 'BOTTOM_UP_TREE':
1144
+ return `${widget.name}:${widget.data.bounds.min}-${widget.data.bounds.max}`;
1145
+ default:
1146
+ Platform.assertNever(widget, 'Unknown AiWidget name');
1147
+ }
1148
+ }
1149
+
1150
+ /**
1151
+ * Returns a new ModelChatMessage where widgets have been deduplicated
1152
+ * across all parts and steps of the message. The first occurrence of each
1153
+ * unique widget (determined by its signature) is preserved.
1154
+ */
1155
+ export function getDeduplicatedWidgetsMessage(message: ModelChatMessage): ModelChatMessage {
1156
+ const seenWidgets = new Set<string>();
1157
+
1158
+ const filterWidgets = (widgets: AiWidget[]): AiWidget[] => {
1159
+ return widgets.filter(widget => {
1160
+ const signature = getWidgetSignature(widget);
1161
+ if (seenWidgets.has(signature)) {
1162
+ return false;
1163
+ }
1164
+ seenWidgets.add(signature);
1165
+ return true;
1166
+ });
1167
+ };
1168
+
1169
+ const deduplicatedParts = message.parts.map(part => {
1170
+ if (part.type === 'widget') {
1171
+ return {
1172
+ ...part,
1173
+ widgets: filterWidgets(part.widgets),
1174
+ };
1175
+ }
1176
+
1177
+ if (part.type === 'step' && part.step.widgets) {
1178
+ return {
1179
+ ...part,
1180
+ step: {
1181
+ ...part.step,
1182
+ widgets: filterWidgets(part.step.widgets),
1183
+ },
1184
+ };
1185
+ }
1186
+
1187
+ return part;
1188
+ });
1189
+
1190
+ return {
1191
+ ...message,
1192
+ parts: deduplicatedParts,
1193
+ };
1194
+ }
1195
+
1116
1196
  async function renderWidgets(
1117
1197
  widgets: AiWidget[]|undefined, options: {wrapperClass?: string} = {}): Promise<Lit.LitTemplate> {
1118
1198
  if (!Root.Runtime.hostConfig.devToolsAiAssistanceV2?.enabled || !widgets || widgets.length === 0) {
@@ -1464,9 +1544,11 @@ export class ChatMessage extends UI.Widget.Widget {
1464
1544
  }
1465
1545
 
1466
1546
  override performUpdate(): Promise<void>|void {
1547
+ const message =
1548
+ this.message.entity === ChatMessageEntity.MODEL ? getDeduplicatedWidgetsMessage(this.message) : this.message;
1467
1549
  this.#view(
1468
1550
  {
1469
- message: this.message,
1551
+ message,
1470
1552
  isLoading: this.isLoading,
1471
1553
  isReadOnly: this.isReadOnly,
1472
1554
  canShowFeedbackForm: this.canShowFeedbackForm,
@@ -70,5 +70,5 @@ export function getButtonLabel(input: {
70
70
  const TARGET_LENGTH = 50;
71
71
  const {truncatedText, moreCharacters} = smartTruncate(input.prompt, TARGET_LENGTH);
72
72
  const promptSuffix = moreCharacters > 0 ? ` (and ${moreCharacters} more characters)` : '';
73
- return `${labelBase} for prompt '${truncatedText}'${promptSuffix}`;
73
+ return `${labelBase} for prompt ${truncatedText}${promptSuffix}`;
74
74
  }
@@ -12,7 +12,13 @@ import * as Lit from '../../../ui/lit/lit.js';
12
12
  import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
13
13
 
14
14
  import chatMessageStyles from './chatMessage.css.js';
15
- import {type ModelChatMessage, renderStep, type Step, titleForStep} from './ChatMessage.js';
15
+ import {
16
+ getDeduplicatedWidgetsMessage,
17
+ type ModelChatMessage,
18
+ renderStep,
19
+ type Step,
20
+ titleForStep
21
+ } from './ChatMessage.js';
16
22
  import {getButtonLabel} from './WalkthroughUtils.js';
17
23
  import walkthroughViewStyles from './walkthroughView.css.js';
18
24
 
@@ -411,6 +417,7 @@ export class WalkthroughView extends UI.Widget.Widget {
411
417
  if (!this.#markdownRenderer) {
412
418
  return;
413
419
  }
420
+ const message = this.#message ? getDeduplicatedWidgetsMessage(this.#message) : null;
414
421
  this.#view(
415
422
  {
416
423
  isLoading: this.#isLoading,
@@ -420,7 +427,7 @@ export class WalkthroughView extends UI.Widget.Widget {
420
427
  isInlined: this.#isInlined,
421
428
  isExpanded: this.#isExpanded,
422
429
  prompt: this.#prompt,
423
- message: this.#message,
430
+ message,
424
431
  handleScroll: this.#handleScroll,
425
432
  },
426
433
  this.#output, this.contentElement);
@@ -171,6 +171,22 @@ const UIStrings = {
171
171
  * @description Text for the header of the tool run section
172
172
  */
173
173
  runTool: 'Run Tool',
174
+ /**
175
+ * @description Context menu action to reveal the tool in the tool list
176
+ */
177
+ revealTool: 'Reveal tool',
178
+ /**
179
+ * @description Context menu action to edit and run the tool
180
+ */
181
+ editAndRun: 'Edit and run',
182
+ /**
183
+ * @description Tooltip for the paste button
184
+ */
185
+ paste: 'Paste',
186
+ /**
187
+ * @description Notice to display when a tool has been unregistered
188
+ */
189
+ toolUnregisteredNotice: 'This tool has been unregistered',
174
190
  } as const;
175
191
  const str_ = i18n.i18n.registerUIStrings('panels/application/WebMCPView.ts', UIStrings);
176
192
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -186,6 +202,7 @@ export interface FilterState {
186
202
  completed?: boolean,
187
203
  error?: boolean,
188
204
  pending?: boolean,
205
+ canceled?: boolean,
189
206
  };
190
207
  }
191
208
 
@@ -198,10 +215,15 @@ export interface FilterMenuButtons {
198
215
  toolTypes: FilterMenuButton;
199
216
  statusTypes: FilterMenuButton;
200
217
  }
218
+ export interface SelectedTool {
219
+ tool: WebMCP.WebMCPModel.Tool;
220
+ parameters?: Record<string, unknown>;
221
+ }
201
222
  export interface ViewInput {
202
223
  tools: WebMCP.WebMCPModel.Tool[];
203
- selectedTool: WebMCP.WebMCPModel.Tool|null;
224
+ selectedTool: SelectedTool|null;
204
225
  onToolSelect: (tool: WebMCP.WebMCPModel.Tool|null) => void;
226
+ onRevealTool: (tool: WebMCP.WebMCPModel.Tool, parameters?: Record<string, unknown>) => void;
205
227
  selectedCall: WebMCP.WebMCPModel.Call|null;
206
228
  onCallSelect: (call: WebMCP.WebMCPModel.Call|null) => void;
207
229
  filters: FilterState;
@@ -210,6 +232,7 @@ export interface ViewInput {
210
232
  onFilterChange: (filters: FilterState) => void;
211
233
  toolCalls: WebMCP.WebMCPModel.Call[];
212
234
  onRunTool: (event: Common.EventTarget.EventTargetEvent<ProtocolMonitor.JSONEditor.Command>) => void;
235
+ onPaste: () => void;
213
236
  }
214
237
 
215
238
  export function filterToolCalls(
@@ -219,13 +242,16 @@ export function filterToolCalls(
219
242
  const statusTypes = filterState.statusTypes;
220
243
  if (statusTypes) {
221
244
  filtered = filtered.filter(call => {
222
- const {completed, error, pending} = statusTypes;
245
+ const {completed, error, pending, canceled} = statusTypes;
223
246
  if (completed && call.result?.status === Protocol.WebMCP.InvocationStatus.Completed) {
224
247
  return true;
225
248
  }
226
249
  if (error && call.result?.status === Protocol.WebMCP.InvocationStatus.Error) {
227
250
  return true;
228
251
  }
252
+ if (canceled && call.result?.status === Protocol.WebMCP.InvocationStatus.Canceled) {
253
+ return true;
254
+ }
229
255
  if (pending && call.result === undefined) {
230
256
  return true;
231
257
  }
@@ -259,63 +285,54 @@ export function filterToolCalls(
259
285
  return filtered;
260
286
  }
261
287
  export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
288
+
289
+ type ToolStats = Map<Protocol.WebMCP.InvocationStatus|undefined, number>;
290
+
262
291
  function calculateToolStats(calls: WebMCP.WebMCPModel.Call[]):
263
- {total: number, completed: number, failed: number, canceled: number, inProgress: number} {
264
- let total = 0, completed = 0, failed = 0, canceled = 0, inProgress = 0;
292
+ {stats: Map<WebMCP.WebMCPModel.Tool, ToolStats>, totals: ToolStats} {
293
+ const stats = new Map<WebMCP.WebMCPModel.Tool, ToolStats>();
294
+ const totals: ToolStats = new Map();
295
+
265
296
  for (const call of calls) {
266
- total++;
267
- if (call.result?.status === Protocol.WebMCP.InvocationStatus.Error) {
268
- failed++;
269
- } else if (call.result?.status === Protocol.WebMCP.InvocationStatus.Canceled) {
270
- canceled++;
271
- } else if (call.result?.status === Protocol.WebMCP.InvocationStatus.Completed) {
272
- completed++;
273
- } else if (call.result === undefined) {
274
- inProgress++;
297
+ let toolStats = stats.get(call.tool);
298
+ if (!toolStats) {
299
+ toolStats = new Map();
300
+ stats.set(call.tool, toolStats);
275
301
  }
302
+ toolStats.set(call.result?.status, (toolStats.get(call.result?.status) ?? 0) + 1);
303
+ totals.set(call.result?.status, (totals.get(call.result?.status) ?? 0) + 1);
276
304
  }
277
- return {total, completed, failed, canceled, inProgress};
305
+ return {totals, stats};
278
306
  }
279
307
 
280
- function getIconGroupsFromStats(toolStats: ReturnType<typeof calculateToolStats>):
281
- IconButton.IconButton.IconWithTextData[] {
282
- const groups = [];
283
- if (toolStats.completed > 0) {
284
- groups.push({
285
- iconName: 'check-circle',
286
- iconColor: 'var(--sys-color-green)',
287
- iconWidth: '16px',
288
- iconHeight: '16px',
289
- text: String(toolStats.completed),
290
- });
308
+ function toolStatsIcon(status: Protocol.WebMCP.InvocationStatus|undefined): {iconName: string, iconColor?: string} {
309
+ switch (status) {
310
+ case Protocol.WebMCP.InvocationStatus.Completed:
311
+ return {iconName: 'check-circle', iconColor: 'var(--sys-color-green)'};
312
+ case Protocol.WebMCP.InvocationStatus.Error:
313
+ return {iconName: 'cross-circle-filled', iconColor: 'var(--sys-color-error)'};
314
+ case Protocol.WebMCP.InvocationStatus.Canceled:
315
+ return {iconName: 'record-stop', iconColor: 'var(--sys-color-on-surface-light)'};
316
+ case undefined:
317
+ return {iconName: 'watch'};
291
318
  }
292
- if (toolStats.failed > 0) {
293
- groups.push({
294
- iconName: 'cross-circle-filled',
295
- iconColor: 'var(--sys-color-error)',
296
- iconWidth: '16px',
297
- iconHeight: '16px',
298
- text: String(toolStats.failed),
299
- });
300
- }
301
- if (toolStats.canceled > 0) {
302
- groups.push({
303
- iconName: 'record-stop',
304
- iconColor: 'var(--sys-color-on-surface-light)',
305
- iconWidth: '16px',
306
- iconHeight: '16px',
307
- text: String(toolStats.canceled),
308
- });
309
- }
310
- if (toolStats.inProgress > 0) {
311
- groups.push({
312
- iconName: 'dots-circle',
313
- iconWidth: '16px',
314
- iconHeight: '16px',
315
- text: String(toolStats.inProgress),
316
- });
317
- }
318
- return groups;
319
+ }
320
+
321
+ function getIconGroupsFromStats(toolStats?: ToolStats):
322
+ Array<IconButton.IconButton.IconWithTextData&{status: Protocol.WebMCP.InvocationStatus | undefined}> {
323
+ const status = [
324
+ Protocol.WebMCP.InvocationStatus.Completed, Protocol.WebMCP.InvocationStatus.Error,
325
+ Protocol.WebMCP.InvocationStatus.Canceled, undefined
326
+ ];
327
+ return status
328
+ .map(status => ({
329
+ ...toolStatsIcon(status),
330
+ iconWidth: 'var(--sys-size-8)',
331
+ iconHeight: 'var(--sys-size-8)',
332
+ text: String(toolStats?.get(status) ?? 0),
333
+ status,
334
+ }))
335
+ .filter(({text}) => text !== '0');
319
336
  }
320
337
 
321
338
  export function parsePayload(payload?: unknown): {
@@ -360,7 +377,7 @@ export function getJSONEditorParameters(tool: WebMCP.WebMCPModel.Tool): {
360
377
  export const DEFAULT_VIEW: View = (input, output, target) => {
361
378
  const tools = input.tools;
362
379
  let editorWidget: ProtocolMonitor.JSONEditor.JSONEditor|null = null;
363
- const stats = calculateToolStats(input.toolCalls);
380
+ const toolStats = calculateToolStats(input.toolCalls);
364
381
  const isFilterActive =
365
382
  Boolean(input.filters.text) || Boolean(input.filters.toolTypes) || Boolean(input.filters.statusTypes);
366
383
  const iconName = (call: WebMCP.WebMCPModel.Call): string => {
@@ -370,7 +387,7 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
370
387
  case Protocol.WebMCP.InvocationStatus.Canceled:
371
388
  return 'record-stop';
372
389
  case undefined:
373
- return 'dots-circle';
390
+ return 'watch';
374
391
  default:
375
392
  return '';
376
393
  }
@@ -387,6 +404,23 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
387
404
  return i18nString(UIStrings.inProgress);
388
405
  }
389
406
  };
407
+ const onIconClick = (toolName: string, status: Protocol.WebMCP.InvocationStatus|undefined): void => {
408
+ let statusTypes: FilterState['statusTypes'] = undefined;
409
+ if (status === Protocol.WebMCP.InvocationStatus.Completed) {
410
+ statusTypes = {completed: true};
411
+ } else if (status === Protocol.WebMCP.InvocationStatus.Error) {
412
+ statusTypes = {error: true};
413
+ } else if (status === Protocol.WebMCP.InvocationStatus.Canceled) {
414
+ statusTypes = {canceled: true};
415
+ } else if (status === undefined) {
416
+ statusTypes = {pending: true};
417
+ }
418
+ input.onFilterChange({
419
+ ...input.filters,
420
+ text: toolName,
421
+ statusTypes,
422
+ });
423
+ };
390
424
  const onToolContextMenu = (event: Event, tool: WebMCP.WebMCPModel.Tool): void => {
391
425
  const contextMenu = new UI.ContextMenu.ContextMenu(event);
392
426
  contextMenu.defaultSection().appendItem(i18nString(UIStrings.copyName), () => {
@@ -412,9 +446,9 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
412
446
  <div class="toolbar-divider"></div>
413
447
  <devtools-toolbar-input type="filter"
414
448
  placeholder=${i18nString(UIStrings.filter)}
415
- .value=${input.filters.text}
416
449
  @change=${(e: CustomEvent<string>) =>
417
- input.onFilterChange({...input.filters, text: e.detail})}>
450
+ input.onFilterChange({...input.filters, text: e.detail})}
451
+ .value=${input.filters.text}>
418
452
  </devtools-toolbar-input>
419
453
  <div class="toolbar-divider"></div>
420
454
  ${input.filterButtons.toolTypes.button.element}
@@ -453,8 +487,34 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
453
487
  'status-error': call.result?.status === Protocol.WebMCP.InvocationStatus.Error,
454
488
  'status-cancelled': call.result?.status === Protocol.WebMCP.InvocationStatus.Canceled,
455
489
  selected: call === input.selectedCall,
456
- })} @click=${() => input.onCallSelect(call)}>
457
- <td>${call.tool.name}</td>
490
+ })} @click=${() => input.onCallSelect(call)}
491
+ @contextmenu=${(e: CustomEvent<UI.ContextMenu.ContextMenu>) => {
492
+ const contextMenu = e.detail;
493
+ const isUnregistered = !input.tools.includes(call.tool);
494
+ contextMenu.defaultSection().appendItem(i18nString(UIStrings.revealTool), () => {
495
+ input.onRevealTool(call.tool);
496
+ }, {jslogContext: 'webmcp.reveal-tool', disabled: isUnregistered});
497
+ contextMenu.defaultSection().appendItem(i18nString(UIStrings.editAndRun), () => {
498
+ const payload = parsePayload(call.input);
499
+ input.onRevealTool(call.tool, payload.valueObject as Record<string, unknown> | undefined);
500
+ }, {jslogContext: 'webmcp.edit-and-run', disabled: isUnregistered});
501
+ }}>
502
+ <td>
503
+ <div class="name-cell">
504
+ <span>${call.tool.name}</span>
505
+ <button class="run-tool-action-button"
506
+ title=${i18nString(UIStrings.editAndRun)}
507
+ aria-label=${i18nString(UIStrings.editAndRun)}
508
+ @click=${(e: Event) => {
509
+ e.stopPropagation();
510
+ const payload = parsePayload(call.input);
511
+ input.onRevealTool(call.tool,
512
+ payload.valueObject as Record<string, unknown> | undefined);
513
+ }}>
514
+ <devtools-icon name="goto-filled"></devtools-icon>
515
+ </button>
516
+ </div>
517
+ </td>
458
518
  <td>
459
519
  <div class="status-cell">
460
520
  ${iconName(call) ? html`<devtools-icon class="small" name=${iconName(call)}></devtools-icon>`
@@ -485,7 +545,7 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
485
545
  <devtools-widget
486
546
  id="webmcp.tool-details"
487
547
  title=${i18nString(UIStrings.toolDetails)}
488
- ${widget(ToolDetailsWidget, {tool: input.selectedCall?.tool})}>
548
+ ${widget(ToolDetailsWidget, {tool: input.selectedCall?.tool, isUnregistered: input.selectedCall ? !input.tools.includes(input.selectedCall.tool) : false})}>
489
549
  </devtools-widget>
490
550
  <devtools-widget
491
551
  id="webmcp.call-inputs"
@@ -506,14 +566,18 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
506
566
  </devtools-split-view>
507
567
  <div class="webmcp-toolbar-container" role="toolbar">
508
568
  <devtools-toolbar class="webmcp-toolbar" role="presentation" wrappable>
509
- <span class="toolbar-text">${i18nString(UIStrings.totalCalls, {PH1: stats.total})}</span>
569
+ <span class="toolbar-text">${i18nString(UIStrings.totalCalls, {PH1: input.toolCalls.length})}</span>
510
570
  <div class="toolbar-divider"></div>
511
- <span class="toolbar-text status-error-text">${i18nString(UIStrings.failed, {PH1: stats.failed})}</span>
571
+ <span class="toolbar-text status-error-text">${
572
+ i18nString(UIStrings.failed,
573
+ {PH1: toolStats.totals.get(Protocol.WebMCP.InvocationStatus.Error) ?? 0})}</span>
512
574
  <div class="toolbar-divider"></div>
513
575
  <span class="toolbar-text status-cancelled-text">${
514
- i18nString(UIStrings.canceledCount, {PH1: stats.canceled})}</span>
576
+ i18nString(UIStrings.canceledCount,
577
+ {PH1: toolStats.totals.get(Protocol.WebMCP.InvocationStatus.Canceled) ?? 0})}</span>
515
578
  <div class="toolbar-divider"></div>
516
- <span class="toolbar-text">${i18nString(UIStrings.inProgressCount, {PH1: stats.inProgress})}</span>
579
+ <span class="toolbar-text">${i18nString(UIStrings.inProgressCount,
580
+ {PH1: toolStats.totals.get(undefined) ?? 0})}</span>
517
581
  </devtools-toolbar>
518
582
  </div>
519
583
  ` : html`
@@ -532,23 +596,26 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
532
596
  ${UI.Widget.widget(UI.EmptyWidget.EmptyWidget, {header: i18nString(UIStrings.noToolsPlaceholderTitle),
533
597
  text: i18nString(UIStrings.noToolsPlaceholder)})}
534
598
  ` : html`
535
- <devtools-list>
536
- ${tools.map(tool => {
537
- const toolStats = calculateToolStats(input.toolCalls.filter(c => c.tool === tool));
538
- const groups = getIconGroupsFromStats(toolStats);
539
- return html`
540
- <div class=${Directives.classMap({'tool-item': true, selected: tool === input.selectedTool})}
541
- @click=${() => input.onToolSelect(tool)}
542
- @contextmenu=${(e: Event) => onToolContextMenu(e, tool)}>
543
- <div class="tool-name-container">
544
- <div class="tool-name source-code">${tool.name}</div>
545
- ${groups.length > 0 ? html`<icon-button .data=${
546
- {groups, compact: false} as IconButton.IconButton.IconButtonData}></icon-button>` : ''}
599
+ <devtools-list class="square-corners">
600
+ ${tools.map(tool => html`
601
+ <div class=${Directives.classMap({'tool-item': true, selected: tool === input.selectedTool?.tool})}
602
+ @click=${() => input.onToolSelect(tool)}
603
+ @contextmenu=${(e: Event) => onToolContextMenu(e, tool)}>
604
+ <div class="tool-name-container">
605
+ <div class="tool-name source-code">${tool.name}</div>
606
+ <div class="tool-icons">
607
+ ${getIconGroupsFromStats(toolStats.stats.get(tool)).map(group => html`
608
+ <icon-button
609
+ .data=${{
610
+ groups: [group],
611
+ compact: false,
612
+ clickHandler: () => onIconClick(tool.name, group.status),
613
+ } as IconButton.IconButton.IconButtonData}
614
+ @click=${(e: Event) => e.stopPropagation()}></icon-button>`)}
547
615
  </div>
548
- <div class="tool-description">${tool.description}</div>
549
616
  </div>
550
- `;
551
- })}
617
+ <div class="tool-description">${tool.description}</div>
618
+ </div>`)}
552
619
  </devtools-list>
553
620
  `}
554
621
  </div>
@@ -565,10 +632,18 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
565
632
  </div>
566
633
  ${input.selectedTool ? html`
567
634
  <div class="sidebar-tool-details">
568
- ${widget(ToolDetailsWidget, {tool: input.selectedTool})}
635
+ ${widget(ToolDetailsWidget, {tool: input.selectedTool.tool})}
569
636
  </div>
570
637
  <div class="section-title">
571
638
  <span>${i18nString(UIStrings.runTool)}</span>
639
+ <div style="flex: auto;"></div>
640
+ <devtools-button
641
+ .iconName=${'import'}
642
+ .size=${Buttons.Button.Size.SMALL}
643
+ .variant=${Buttons.Button.Variant.TEXT}
644
+ title=${i18nString(UIStrings.paste)}
645
+ @click=${input.onPaste}
646
+ >${i18nString(UIStrings.paste)}</devtools-button>
572
647
  </div>
573
648
  <devtools-widget
574
649
  class="json-editor-widget"
@@ -576,9 +651,12 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
576
651
  displayTargetSelector: false,
577
652
  displayCommandInput: false,
578
653
  displayToolbar: false,
579
- ...getJSONEditorParameters(input.selectedTool),
580
- commandToDisplay: input.selectedTool.name,
581
- })}
654
+ ...getJSONEditorParameters(input.selectedTool.tool),
655
+ commandToDisplay: {
656
+ command: input.selectedTool.tool.name,
657
+ parameters: input.selectedTool.parameters || {}
658
+ },
659
+ })}
582
660
  ${UI.Widget.widgetRef(ProtocolMonitor.JSONEditor.JSONEditor, e => { editorWidget = e; })}
583
661
  @submiteditor=${(e: CustomEvent<ProtocolMonitor.JSONEditor.Command>) => input.onRunTool({data: e.detail})}
584
662
  ></devtools-widget>
@@ -592,7 +670,7 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
592
670
  const params = editorWidget.getParameters();
593
671
  input.onRunTool({
594
672
  data: {
595
- command: input.selectedTool.name,
673
+ command: input.selectedTool.tool.name,
596
674
  parameters: params,
597
675
  } as ProtocolMonitor.JSONEditor.Command
598
676
  });
@@ -608,7 +686,7 @@ export const DEFAULT_VIEW: View = (input, output, target) => {
608
686
 
609
687
  export class WebMCPView extends UI.Widget.VBox {
610
688
  readonly #view: View;
611
- #selectedTool: WebMCP.WebMCPModel.Tool|null = null;
689
+ #selectedTool: SelectedTool|null = null;
612
690
  #selectedCall: WebMCP.WebMCPModel.Call|null = null;
613
691
 
614
692
  #filterState: FilterState = {
@@ -686,11 +764,11 @@ export class WebMCPView extends UI.Widget.VBox {
686
764
  }
687
765
 
688
766
  #showStatusTypesContextMenu(contextMenu: UI.ContextMenu.ContextMenu): void {
689
- const toggle = (key: 'completed'|'error'|'pending'): void => {
767
+ const toggle = (key: 'completed'|'error'|'pending'|'canceled'): void => {
690
768
  const current = this.#filterState.statusTypes ?? {};
691
769
  const next = {...current, [key]: !current[key]};
692
770
  let statusTypesToPass: FilterState['statusTypes'] = next;
693
- if (!next.completed && !next.error && !next.pending) {
771
+ if (!next.completed && !next.error && !next.pending && !next.canceled) {
694
772
  statusTypesToPass = undefined;
695
773
  }
696
774
  this.#handleFilterChange({...this.#filterState, statusTypes: statusTypesToPass});
@@ -702,6 +780,9 @@ export class WebMCPView extends UI.Widget.VBox {
702
780
  contextMenu.defaultSection().appendCheckboxItem(
703
781
  i18nString(UIStrings.error), () => toggle('error'),
704
782
  {checked: this.#filterState.statusTypes?.['error'] ?? false, jslogContext: 'webmcp.error'});
783
+ contextMenu.defaultSection().appendCheckboxItem(
784
+ i18nString(UIStrings.canceled), () => toggle('canceled'),
785
+ {checked: this.#filterState.statusTypes?.['canceled'] ?? false, jslogContext: 'webmcp.canceled'});
705
786
  contextMenu.defaultSection().appendCheckboxItem(
706
787
  i18nString(UIStrings.pending), () => toggle('pending'),
707
788
  {checked: this.#filterState.statusTypes?.['pending'] ?? false, jslogContext: 'webmcp.pending'});
@@ -721,7 +802,7 @@ export class WebMCPView extends UI.Widget.VBox {
721
802
  }
722
803
 
723
804
  #toolsRemoved(event: Common.EventTarget.EventTargetEvent<readonly WebMCP.WebMCPModel.Tool[]>): void {
724
- if (this.#selectedTool && event.data.includes(this.#selectedTool)) {
805
+ if (this.#selectedTool && event.data.includes(this.#selectedTool.tool)) {
725
806
  this.#selectedTool = null;
726
807
  }
727
808
  this.requestUpdate();
@@ -763,7 +844,11 @@ export class WebMCPView extends UI.Widget.VBox {
763
844
  tools,
764
845
  selectedTool: this.#selectedTool,
765
846
  onToolSelect: tool => {
766
- this.#selectedTool = tool;
847
+ this.#selectedTool = tool ? {tool} : null;
848
+ this.requestUpdate();
849
+ },
850
+ onRevealTool: (tool, parameters) => {
851
+ this.#selectedTool = {tool, parameters};
767
852
  this.requestUpdate();
768
853
  },
769
854
  selectedCall: this.#selectedCall,
@@ -778,7 +863,21 @@ export class WebMCPView extends UI.Widget.VBox {
778
863
  onFilterChange: this.#handleFilterChange,
779
864
  onRunTool: event => {
780
865
  if (this.#selectedTool) {
781
- void this.#selectedTool.invoke(event.data.parameters || {});
866
+ void this.#selectedTool.tool.invoke(event.data.parameters || {});
867
+ }
868
+ },
869
+ onPaste: async () => {
870
+ try {
871
+ const text = await navigator.clipboard.readText();
872
+ const json = JSON.parse(text);
873
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) {
874
+ throw new Error('Pasted JSON must be an object');
875
+ }
876
+ if (this.#selectedTool) {
877
+ this.#selectedTool.parameters = json as Record<string, unknown>;
878
+ this.requestUpdate();
879
+ }
880
+ } catch {
782
881
  }
783
882
  },
784
883
  };
@@ -964,6 +1063,7 @@ export class PayloadWidget extends UI.Widget.Widget {
964
1063
 
965
1064
  export interface ToolDetailsViewInput {
966
1065
  tool: WebMCP.WebMCPModel.Tool|null|undefined;
1066
+ isUnregistered?: boolean;
967
1067
  origin: SDK.DOMModel.DOMNode|StackTrace.StackTrace.StackTrace|undefined;
968
1068
  highlightNode: (node: SDK.DOMModel.DOMNode) => void;
969
1069
  clearHighlight: () => void;
@@ -1020,6 +1120,16 @@ const TOOL_DETAILS_VIEW = (input: ToolDetailsViewInput, output: undefined, targe
1020
1120
  {stackTrace: origin, options: { expandable: true}})}
1021
1121
  </div>` : nothing}
1022
1122
  </div>
1123
+ ${input.isUnregistered ? html`
1124
+ <div class="call-to-action">
1125
+ <div class="call-to-action-body">
1126
+ <div class="explanation">
1127
+ <devtools-icon class="inline-icon medium" name="warning-filled"></devtools-icon>
1128
+ ${i18nString(UIStrings.toolUnregisteredNotice)}
1129
+ </div>
1130
+ </div>
1131
+ </div>
1132
+ ` : nothing}
1023
1133
  `, target);
1024
1134
  };
1025
1135
  // clang-format on
@@ -1027,6 +1137,7 @@ const TOOL_DETAILS_VIEW = (input: ToolDetailsViewInput, output: undefined, targe
1027
1137
  export class ToolDetailsWidget extends UI.Widget.Widget {
1028
1138
  #tool: WebMCP.WebMCPModel.Tool|null|undefined = null;
1029
1139
  #origin: SDK.DOMModel.DOMNode|StackTrace.StackTrace.StackTrace|undefined;
1140
+ #isUnregistered = false;
1030
1141
 
1031
1142
  #view: typeof TOOL_DETAILS_VIEW;
1032
1143
 
@@ -1035,6 +1146,17 @@ export class ToolDetailsWidget extends UI.Widget.Widget {
1035
1146
  this.#view = view;
1036
1147
  }
1037
1148
 
1149
+ set isUnregistered(isUnregistered: boolean) {
1150
+ if (this.#isUnregistered === isUnregistered) {
1151
+ return;
1152
+ }
1153
+ this.#isUnregistered = isUnregistered;
1154
+ this.requestUpdate();
1155
+ }
1156
+
1157
+ get isUnregistered(): boolean {
1158
+ return this.#isUnregistered;
1159
+ }
1038
1160
  set tool(tool: WebMCP.WebMCPModel.Tool|null|undefined) {
1039
1161
  if (this.#tool === tool) {
1040
1162
  return;
@@ -1076,6 +1198,7 @@ export class ToolDetailsWidget extends UI.Widget.Widget {
1076
1198
  override performUpdate(): void {
1077
1199
  const viewInput = {
1078
1200
  tool: this.#tool,
1201
+ isUnregistered: this.#isUnregistered,
1079
1202
  origin: this.#origin,
1080
1203
  highlightNode: this.#highlightNode,
1081
1204
  clearHighlight: this.#clearHighlight,