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
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/*" ]
}
221 changes: 221 additions & 0 deletions front_end/panels/livemate/LivematePanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// 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 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: pick component button and breadcrumb
const topRow = document.createElement('div');
topRow.setAttribute('style', 'display: flex; align-items: center; gap: 8px;');

// Pick component button
const pickComponentButton = document.createElement('button');
pickComponentButton.textContent = 'Pick component';
pickComponentButton.setAttribute('style', 'padding: 4px 12px; cursor: pointer;');
pickComponentButton.addEventListener('click', () => {
(bridge as unknown as {send: (event: string) => void}).send('startInspectingHost');
});
topRow.appendChild(pickComponentButton);

// 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(-5);

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('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);

// 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: HTMLTextAreaElement = 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;');

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

// 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', async () => {
await sendCommandToMetro(queryInput, currentHierarchy);
});

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

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

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

async function sendCommandToMetro(
input: HTMLTextAreaElement,
currentHierarchy: Array<{name: string}> = [],
timeoutMs = 5000
): Promise<{success: boolean; output?: string; error?: string}> {
const query = input.value;
let prompt;
if (query.trim()) {
prompt = query;
if (currentHierarchy.length > 0) {
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}`;
}
}

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

try {
const response = await fetch('http://localhost:8081/livemate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({prompt}),
signal: controller.signal,
});

clearTimeout(timeoutId);

if (!response.ok) {
const errorText = await response.text();
input.value = '';
return {
success: false,
error: `HTTP ${response.status}: ${errorText}`,
};
}

const result = await response.json();
return result;
} catch (e) {
clearTimeout(timeoutId);

if (e instanceof Error && e.name === 'AbortError') {
input.value = '';
return {
success: false,
error: 'Request timeout',
};
}

input.value = '';
return {
success: false,
error: e instanceof Error ? e.message : 'Unknown error',
};
}
}
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