diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 591a6e1e837..317517f56b0 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -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", diff --git a/config/gni/devtools_image_files.gni b/config/gni/devtools_image_files.gni index 261cf0870c7..874faad6a73 100644 --- a/config/gni/devtools_image_files.gni +++ b/config/gni/devtools_image_files.gni @@ -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", diff --git a/front_end/Images/react_native/whats-new-083.jpg b/front_end/Images/react_native/whats-new-083.jpg new file mode 100644 index 00000000000..969df54a303 Binary files /dev/null and b/front_end/Images/react_native/whats-new-083.jpg differ diff --git a/front_end/core/host/InspectorFrontendHost.ts b/front_end/core/host/InspectorFrontendHost.ts index 2dcfba71481..8d836194cbf 100644 --- a/front_end/core/host/InspectorFrontendHost.ts +++ b/front_end/core/host/InspectorFrontendHost.ts @@ -141,6 +141,9 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI { bringToFront(): void { } + sendToDevmate(prompt: string): void { + } + closeWindow(): void { } diff --git a/front_end/core/host/InspectorFrontendHostAPI.ts b/front_end/core/host/InspectorFrontendHostAPI.ts index 344dc54a478..af869fb04fa 100644 --- a/front_end/core/host/InspectorFrontendHostAPI.ts +++ b/front_end/core/host/InspectorFrontendHostAPI.ts @@ -297,6 +297,8 @@ export interface InspectorFrontendHostAPI { bringToFront(): void; + sendToDevmate(prompt: string): void; + closeWindow(): void; copyText(text: string|null|undefined): void; @@ -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 diff --git a/front_end/entrypoints/rn_fusebox/BUILD.gn b/front_end/entrypoints/rn_fusebox/BUILD.gn index 02c7dab7442..afa400234f3 100644 --- a/front_end/entrypoints/rn_fusebox/BUILD.gn +++ b/front_end/entrypoints/rn_fusebox/BUILD.gn @@ -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", diff --git a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts index 154368f7340..de463657c94 100644 --- a/front_end/entrypoints/rn_fusebox/rn_fusebox.ts +++ b/front_end/entrypoints/rn_fusebox/rn_fusebox.ts @@ -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'; diff --git a/front_end/panels/livemate/BUILD.gn b/front_end/panels/livemate/BUILD.gn new file mode 100644 index 00000000000..a5c9116e3c1 --- /dev/null +++ b/front_end/panels/livemate/BUILD.gn @@ -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/*" ] +} diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts new file mode 100644 index 00000000000..bbf7c979f2a --- /dev/null +++ b/front_end/panels/livemate/LivematePanel.ts @@ -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); + + + + } +} diff --git a/front_end/panels/livemate/livemate-meta.ts b/front_end/panels/livemate/livemate-meta.ts new file mode 100644 index 00000000000..ddde712765d --- /dev/null +++ b/front_end/panels/livemate/livemate-meta.ts @@ -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 { + 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(); + }, +}); diff --git a/front_end/panels/livemate/livemate.ts b/front_end/panels/livemate/livemate.ts new file mode 100644 index 00000000000..2b3fb11f492 --- /dev/null +++ b/front_end/panels/livemate/livemate.ts @@ -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, +}; diff --git a/front_end/panels/livemate/livematePanel.css b/front_end/panels/livemate/livematePanel.css new file mode 100644 index 00000000000..93e99b3b03a --- /dev/null +++ b/front_end/panels/livemate/livematePanel.css @@ -0,0 +1,88 @@ +.livemate-panel { + display: flex; + flex-direction: column; + height: 100%; + padding: 16px; + background-color: var(--sys-color-cdt-base-container); + box-sizing: border-box; +} + +.livemate-prompt-section { + display: flex; + flex-direction: column; + gap: 12px; + max-width: 600px; + width: 100%; + margin: 0 auto; +} + +.livemate-prompt-input { + width: 100%; + min-height: 120px; + padding: 12px; + border: 1px solid var(--sys-color-divider); + border-radius: 8px; + font-family: inherit; + font-size: 14px; + line-height: 1.5; + resize: vertical; + background-color: var(--sys-color-surface); + color: var(--sys-color-on-surface); + box-sizing: border-box; +} + +.livemate-prompt-input::placeholder { + color: var(--sys-color-state-disabled-container); +} + +.livemate-prompt-input:focus { + outline: none; + border-color: var(--sys-color-primary); + box-shadow: 0 0 0 1px var(--sys-color-primary); +} + +.livemate-send-button { + align-self: flex-start; + padding: 8px 16px; + font-size: 13px; + font-weight: 500; + border-radius: 4px; + border: none; + background-color: var(--sys-color-primary); + color: var(--sys-color-on-primary); + cursor: pointer; + transition: background-color 0.15s ease; +} + +.livemate-send-button:hover { + background-color: var(--sys-color-primary-hover); +} + +.livemate-send-button:active { + background-color: var(--sys-color-primary-pressed); +} + +.livemate-status { + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; +} + +.livemate-status:empty { + display: none; +} + +.livemate-status.error { + background-color: var(--sys-color-error-container); + color: var(--sys-color-error); +} + +.livemate-status.success { + background-color: var(--sys-color-green-container); + color: var(--sys-color-green); +} + +.livemate-status.pending { + background-color: var(--sys-color-tonal-container); + color: var(--sys-color-primary); +} diff --git a/front_end/panels/react_devtools/BUILD.gn b/front_end/panels/react_devtools/BUILD.gn index 07d928f8403..883e974fa27 100644 --- a/front_end/panels/react_devtools/BUILD.gn +++ b/front_end/panels/react_devtools/BUILD.gn @@ -38,6 +38,7 @@ devtools_entrypoint("bundle") { visibility = [ ":*", "../../entrypoints/*", + "../livemate/*", ] visibility += devtools_panels_visibility diff --git a/front_end/panels/react_devtools/ReactDevToolsViewBase.ts b/front_end/panels/react_devtools/ReactDevToolsViewBase.ts index 4f6eee89c63..aaf2caf625e 100644 --- a/front_end/panels/react_devtools/ReactDevToolsViewBase.ts +++ b/front_end/panels/react_devtools/ReactDevToolsViewBase.ts @@ -75,7 +75,7 @@ function viewElementSourceFunction(source: ReactDevToolsTypes.Source, symbolicat export class ReactDevToolsViewBase extends UI.View.SimpleView implements SDK.TargetManager.SDKModelObserver { readonly #tab: string; - #model: ReactDevToolsModel | null = null; + model: ReactDevToolsModel | null = null; constructor( tab: 'components' | 'profiler', @@ -92,7 +92,7 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements } modelAdded(model: ReactDevToolsModel): void { - this.#model = model; + this.model = model; model.addEventListener( ReactDevToolsModelEvents.INITIALIZATION_COMPLETED, @@ -113,7 +113,7 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements if (model.isInitialized()) { // Already initialized from another rendered React DevTools panel - render // from initialized state - this.#renderDevToolsView(); + this.renderDevToolsView(); } else { // Once initialized, it will emit InitializationCompleted event model.ensureInitialized(); @@ -139,7 +139,7 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements } #handleInitializationCompleted(): void { - this.#renderDevToolsView(); + this.renderDevToolsView(); } #handleInitializationFailed({data: errorMessage}: ReactDevToolsInitializationFailedEvent): void { @@ -150,10 +150,10 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements this.#renderLoader(); } - #renderDevToolsView(): void { - this.#clearView(); + renderDevToolsView(): void { + this.clearView(); - const model = this.#model; + const model = this.model; if (model === null) { throw new Error('Attempted to render React DevTools panel, but the model was null'); } @@ -171,7 +171,7 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements } #renderLoader(): void { - this.#clearView(); + this.clearView(); const loaderContainer = document.createElement('div'); loaderContainer.setAttribute('style', 'display: flex; flex: 1; justify-content: center; align-items: center'); @@ -184,7 +184,7 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements } #renderErrorView(errorMessage: string): void { - this.#clearView(); + this.clearView(); const errorContainer = document.createElement('div'); errorContainer.setAttribute('style', 'display: flex; flex: 1; flex-direction: column; justify-content: center; align-items: center'); @@ -210,7 +210,7 @@ export class ReactDevToolsViewBase extends UI.View.SimpleView implements } } - #clearView(): void { + clearView(): void { this.contentElement.removeChildren(); } } diff --git a/front_end/panels/rn_welcome/RNWelcome.ts b/front_end/panels/rn_welcome/RNWelcome.ts index 093de35df4d..8b15afa1846 100644 --- a/front_end/panels/rn_welcome/RNWelcome.ts +++ b/front_end/panels/rn_welcome/RNWelcome.ts @@ -27,7 +27,7 @@ const UIStrings = { /** @description "What's new" link */ whatsNewLabel: 'What\'s new', /** @description Description for sharing the session ID of the current session with the user */ - sessionIdMessage: '[FB-only] The ID for this React Native DevTools session is:', + sessionIdMessage: '[FB-only] React Native DevTools session ID:', /** @description "Debugging Basics" title (docs item 1) */ docsDebuggingBasics: 'Debugging Basics', /** @description "Debugging Basics" item detail */ @@ -40,6 +40,10 @@ const UIStrings = { docsNativeDebugging: 'Native Debugging', /** @description "Native Debugging" item detail */ docsNativeDebuggingDetail: 'Find out more about native debugging tools', + /** @description Title for the "What's New" highlighted item */ + whatsNewHighlightTitle: 'React Native 0.83 - Performance & Network debugging, improved desktop experience', + /** @description Detail for the "What's New" highlighted item */ + whatsNewHighlightDetail: 'Learn about the latest debugging features in 0.83', } as const; const str_ = i18n.i18n.registerUIStrings('panels/rn_welcome/RNWelcome.ts', UIStrings); @@ -127,6 +131,10 @@ export class RNWelcomeImpl extends UI.Widget.VBox implements '../../Images/react_native/welcomeIcon.png', import.meta.url, ).toString(); + const whatsNewImageUrl = new URL( + '../../Images/react_native/whats-new-083.jpg', + import.meta.url, + ).toString(); const docsImage1Url = new URL( '../../Images/react_native/learn-debugging-basics.jpg', import.meta.url, @@ -188,6 +196,16 @@ export class RNWelcomeImpl extends UI.Widget.VBox implements ${showDocs ? html`
+
+

What's new

+ +

Learn