Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ grd_files_release_sources = [
"front_end/Images/psychiatry.svg",
"front_end/Images/react_native/learn-debugging-basics.jpg",
"front_end/Images/react_native/learn-native-debugging.jpg",
"front_end/Images/react_native/whats-new-083.jpg",
"front_end/Images/react_native/learn-react-native-devtools.jpg",
"front_end/Images/react_native/welcomeIcon.png",
"front_end/Images/record-start.svg",
Expand Down
1 change: 1 addition & 0 deletions config/gni/devtools_image_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ devtools_image_files = [
"popoverArrows.png",
"react_native/learn-debugging-basics.jpg",
"react_native/learn-native-debugging.jpg",
"react_native/whats-new-083.jpg",
"react_native/learn-react-native-devtools.jpg",
"react_native/welcomeIcon.png",
"toolbarResizerVertical.png",
Expand Down
Binary file added front_end/Images/react_native/whats-new-083.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions front_end/core/host/InspectorFrontendHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
bringToFront(): void {
}

sendToDevmate(prompt: string): void {
}

closeWindow(): void {
}

Expand Down
8 changes: 8 additions & 0 deletions front_end/core/host/InspectorFrontendHostAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ export interface InspectorFrontendHostAPI {

bringToFront(): void;

sendToDevmate(prompt: string): void;

closeWindow(): void;

copyText(text: string|null|undefined): void;
Expand Down Expand Up @@ -453,6 +455,12 @@ export interface SyncInformation {
isSyncPaused?: boolean;
}

export interface FileWriteResult {
success: boolean;
path: string|null;
error?: string;
}

/**
* Enum for recordPerformanceHistogram
* Warning: There is another definition of this enum in the DevTools code
Expand Down
1 change: 1 addition & 0 deletions front_end/entrypoints/rn_fusebox/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ devtools_entrypoint("entrypoint") {
"../../panels/react_devtools:components_meta",
"../../panels/react_devtools:profiler_meta",
"../../panels/rn_welcome:meta",
"../../panels/livemate:meta",
"../../panels/security:meta",
"../../panels/sensors:meta",
"../../panels/timeline:meta",
Expand Down
1 change: 1 addition & 0 deletions front_end/entrypoints/rn_fusebox/rn_fusebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../../panels/network/network-meta.js';
import '../../panels/react_devtools/react_devtools_components-meta.js';
import '../../panels/react_devtools/react_devtools_profiler-meta.js';
import '../../panels/rn_welcome/rn_welcome-meta.js';
import '../../panels/livemate/livemate-meta.js';
import '../../panels/timeline/timeline-meta.js';

import * as Host from '../../core/host/host.js';
Expand Down
52 changes: 52 additions & 0 deletions front_end/panels/livemate/BUILD.gn
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# Copyright 2024 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("../../../scripts/build/ninja/devtools_entrypoint.gni")
import("../../../scripts/build/ninja/devtools_module.gni")
import("../../../scripts/build/ninja/generate_css.gni")
import("../visibility.gni")

generate_css("css_files") {
sources = [ "livematePanel.css" ]
}

devtools_module("livemate") {
sources = [ "LivematePanel.ts" ]

deps = [
"../../ui/legacy:bundle",
"../react_devtools:bundle",
]
}

devtools_entrypoint("bundle") {
entrypoint = "livemate.ts"

deps = [
":css_files",
"../react_devtools:bundle",
":livemate",
]

visibility = [
":*",
"../../entrypoints/*",
]

visibility += devtools_panels_visibility
}

devtools_entrypoint("meta") {
entrypoint = "livemate-meta.ts"

deps = [
":bundle",

"../../core/i18n:bundle",
"../../ui/legacy:bundle",
]

visibility = [ "../../entrypoints/*" ]
}
178 changes: 178 additions & 0 deletions front_end/panels/livemate/LivematePanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import { ReactDevToolsViewBase } from '../react_devtools/ReactDevToolsViewBase.js';

import livematePanelStyles from './livematePanel.css.js';

let livematePanelInstance: LivematePanel;

const UIStrings = {
/**
*@description Title of the React DevTools view
*/
title: '⚛️ Livemate',
} as const;
const str_ = i18n.i18n.registerUIStrings(
'panels/livemate/LivematePanel.ts',
UIStrings
);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class LivematePanel extends ReactDevToolsViewBase {
static instance(): LivematePanel {
if (!livematePanelInstance) {
livematePanelInstance = new LivematePanel();
}
return livematePanelInstance;
}

constructor() {
super('components', i18nString(UIStrings.title));
this.registerRequiredCSS(livematePanelStyles);
}

override renderDevToolsView(): void {
this.clearView();

this.contentElement.classList.add('livemate-panel');

const model = this.model;
if (model === null) {
throw new Error('Attempted to render React DevTools panel, but the model was null');
}

const bridge = model.getBridgeOrThrow();

// Create outer wrapper for centering
const outerWrapper = document.createElement('div');
outerWrapper.setAttribute('style', 'display: flex; justify-content: center; align-items: center; min-height: 100%;');

// Create toolbar container
const toolbarContainer = document.createElement('div');
toolbarContainer.setAttribute('style', 'display: flex; flex-direction: column; padding: 20px; gap: 12px; max-width: 800px; width: 100%; margin: 0 20px; border: 1px solid var(--sys-color-divider); border-radius: 8px; background: var(--sys-color-surface);');

// First row: breadcrumb
const topRow = document.createElement('div');
topRow.setAttribute('style', 'display: flex; align-items: center; gap: 8px;');

// Breadcrumb view
const breadcrumb = document.createElement('div');
breadcrumb.setAttribute('style', 'flex: 1; font-family: monospace; font-size: 12px; color: var(--sys-color-on-surface); display: flex; align-items: center; gap: 4px; flex-wrap: wrap;');

// Selected component box
const selectedComponentBox = document.createElement('div');
selectedComponentBox.setAttribute('style', 'padding: 4px 8px; border: 1px solid var(--sys-color-divider); border-radius: 4px; background: var(--sys-color-surface-variant); font-family: monospace; font-size: 12px; color: var(--sys-color-on-surface);');
selectedComponentBox.textContent = '';

// Track the current hierarchy for prompt context
let currentHierarchy: Array<{name: string}> = [];

// Function to update breadcrumb with component data
const updateBreadcrumb = (components: Array<{name: string}>): void => {
breadcrumb.innerHTML = '';

if (components.length === 0) {
return;
}

// Set the selected component to the first one (most specific)
selectedComponentBox.textContent = components[0].name;

// Show remaining components as breadcrumb (skip the first since it's in the selected box)
const breadcrumbComponents = components.slice(1);

breadcrumbComponents.forEach((component, index) => {
const componentSpan = document.createElement('span');
componentSpan.textContent = component.name;
componentSpan.setAttribute('style', 'cursor: pointer; color: var(--sys-color-primary); text-decoration: underline;');
componentSpan.addEventListener('click', () => {
selectedComponentBox.textContent = component.name;
});
componentSpan.addEventListener('mouseenter', () => {
componentSpan.style.opacity = '0.7';
});
componentSpan.addEventListener('mouseleave', () => {
componentSpan.style.opacity = '1';
});

breadcrumb.appendChild(componentSpan);

if (index < breadcrumbComponents.length - 1) {
const separator = document.createElement('span');
separator.textContent = '>';
separator.setAttribute('style', 'color: var(--sys-color-on-surface); opacity: 0.6;');
breadcrumb.appendChild(separator);
}
});
};

// Listen for component data from React DevTools
bridge.addListener('viewDataAtPoint', (data: unknown) => {
currentHierarchy = data as Array<{name: string}>;
updateBreadcrumb(currentHierarchy);
});

topRow.appendChild(breadcrumb);
topRow.appendChild(selectedComponentBox);

// Second row: AI query input and send button
const bottomRow = document.createElement('div');
bottomRow.setAttribute('style', 'display: flex; align-items: center; gap: 8px;');

// AI query text box
const queryInput = document.createElement('textarea');
queryInput.setAttribute('placeholder', 'Query to modify component...');
queryInput.setAttribute('style', 'flex: 1; padding: 12px 16px; border: 1px solid var(--sys-color-divider); border-radius: 4px; background: var(--sys-color-cdt-base-container); color: var(--sys-color-on-surface); font-size: 14px; min-height: 100px; resize: vertical; font-family: inherit;');

// Function to send query to Devmate
const sendQueryToDevmate = (): void => {
const query = queryInput.value;
if (query.trim()) {
// Build the prompt with focused component and hierarchy information
let prompt = query;
if (currentHierarchy.length > 0) {
// The focused component is the last item in the hierarchy (leaf node)
const focusedComponent = currentHierarchy[currentHierarchy.length - 1].name;
const hierarchyStr = currentHierarchy.map(c => c.name).join(' > ');
prompt = `Focused component: ${focusedComponent}\nComponent hierarchy: ${hierarchyStr}\n\nQuery: ${query}`;
}
(
Host.InspectorFrontendHost.InspectorFrontendHostInstance as unknown as {
sendToDevmate: (prompt: string) => void,
}
).sendToDevmate(prompt);
queryInput.value = '';
}
};

// Handle Enter key to send prompt (Shift+Enter for newline)
queryInput.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendQueryToDevmate();
}
});

// Send to devmate button
const sendButton = document.createElement('button');
sendButton.textContent = 'Send to Devmate';
sendButton.setAttribute('style', 'padding: 4px 12px; cursor: pointer; align-self: flex-end;');
sendButton.addEventListener('click', sendQueryToDevmate);

bottomRow.appendChild(queryInput);
bottomRow.appendChild(sendButton);

toolbarContainer.appendChild(topRow);
toolbarContainer.appendChild(bottomRow);

outerWrapper.appendChild(toolbarContainer);
this.contentElement.appendChild(outerWrapper);



}
}
43 changes: 43 additions & 0 deletions front_end/panels/livemate/livemate-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import type * as Livemate from './livemate.js';

const UIStrings = {
/**
*@description Title of the Livemate panel
*/
livemate: 'Livemate',
/**
*@description Command for showing the Livemate panel
*/
showLivemate: 'Show Livemate',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/livemate/livemate-meta.ts', UIStrings);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);

let loadedLivemateModule: (typeof Livemate | undefined);

async function loadLivemateModule(): Promise<typeof Livemate> {
if (!loadedLivemateModule) {
loadedLivemateModule = await import('./livemate.js');
}
return loadedLivemateModule;
}

UI.ViewManager.registerViewExtension({
location: UI.ViewManager.ViewLocationValues.PANEL,
id: 'livemate',
title: i18nLazyString(UIStrings.livemate),
commandPrompt: i18nLazyString(UIStrings.showLivemate),
order: 100,
async loadView() {
const Livemate = await loadLivemateModule();
return Livemate.LivematePanel.LivematePanel.instance();
},
});
10 changes: 10 additions & 0 deletions front_end/panels/livemate/livemate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as LivematePanel from './LivematePanel.js';

export {
LivematePanel,
};
Loading
Loading