From afda86f2390b9e1281102545e335af945a205b12 Mon Sep 17 00:00:00 2001 From: devan Date: Wed, 26 Nov 2025 11:22:52 -0800 Subject: [PATCH 1/9] Initial livemate panel entrypoint --- front_end/entrypoints/rn_fusebox/BUILD.gn | 1 + .../entrypoints/rn_fusebox/rn_fusebox.ts | 1 + front_end/panels/livemate/BUILD.gn | 47 +++++++++++++++++++ front_end/panels/livemate/LivematePanel.ts | 36 ++++++++++++++ front_end/panels/livemate/livemate-meta.ts | 43 +++++++++++++++++ front_end/panels/livemate/livemate.ts | 10 ++++ front_end/panels/livemate/livematePanel.css | 20 ++++++++ 7 files changed, 158 insertions(+) create mode 100644 front_end/panels/livemate/BUILD.gn create mode 100644 front_end/panels/livemate/LivematePanel.ts create mode 100644 front_end/panels/livemate/livemate-meta.ts create mode 100644 front_end/panels/livemate/livemate.ts create mode 100644 front_end/panels/livemate/livematePanel.css 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..d94a87a79db --- /dev/null +++ b/front_end/panels/livemate/BUILD.gn @@ -0,0 +1,47 @@ +# 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" ] +} + +devtools_entrypoint("bundle") { + entrypoint = "livemate.ts" + + deps = [ + ":css_files", + ":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..e377d7daef0 --- /dev/null +++ b/front_end/panels/livemate/LivematePanel.ts @@ -0,0 +1,36 @@ +// 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 UI from '../../ui/legacy/legacy.js'; +import livematePanelStyles from './livematePanel.css.js'; + +let livematePanelInstance: LivematePanel; + +export class LivematePanel extends UI.Widget.VBox { + static instance(): LivematePanel { + if (!livematePanelInstance) { + livematePanelInstance = new LivematePanel(); + } + return livematePanelInstance; + } + + private constructor() { + super(true, true); + this.registerRequiredCSS(livematePanelStyles); + this.contentElement.classList.add('livemate-panel'); + } + + override wasShown(): void { + super.wasShown(); + this.renderContent(); + } + + private renderContent(): void { + this.contentElement.removeChildren(); + + const header = this.contentElement.createChild('div', 'livemate-header'); + header.textContent = 'Livemate Panel'; + } +} 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..9e0a0eb20f6 --- /dev/null +++ b/front_end/panels/livemate/livematePanel.css @@ -0,0 +1,20 @@ +.livemate-panel { + display: flex; + flex-direction: column; + overflow: auto; + padding: 12px; + background-color: var(--sys-color-cdt-base-container); +} + +.livemate-header { + font-size: 18px; + font-weight: 500; + margin-bottom: 16px; + color: var(--sys-color-on-surface); +} + +.livemate-content { + font-size: 14px; + color: var(--sys-color-on-surface); + line-height: 1.6; +} From 76e9c982ed37c75c2da5c30c87ce40975482107e Mon Sep 17 00:00:00 2001 From: devan Date: Mon, 8 Dec 2025 09:19:38 -0700 Subject: [PATCH 2/9] Expose ReactDevToolsViewBase configs and wire up bridge --- front_end/panels/livemate/BUILD.gn | 7 +- front_end/panels/livemate/LivematePanel.ts | 65 +++++++++++++++---- front_end/panels/react_devtools/BUILD.gn | 1 + .../react_devtools/ReactDevToolsViewBase.ts | 20 +++--- 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/front_end/panels/livemate/BUILD.gn b/front_end/panels/livemate/BUILD.gn index d94a87a79db..a5c9116e3c1 100644 --- a/front_end/panels/livemate/BUILD.gn +++ b/front_end/panels/livemate/BUILD.gn @@ -15,7 +15,10 @@ generate_css("css_files") { devtools_module("livemate") { sources = [ "LivematePanel.ts" ] - deps = [ "../../ui/legacy:bundle" ] + deps = [ + "../../ui/legacy:bundle", + "../react_devtools:bundle", + ] } devtools_entrypoint("bundle") { @@ -23,6 +26,7 @@ devtools_entrypoint("bundle") { deps = [ ":css_files", + "../react_devtools:bundle", ":livemate", ] @@ -39,6 +43,7 @@ devtools_entrypoint("meta") { deps = [ ":bundle", + "../../core/i18n:bundle", "../../ui/legacy:bundle", ] diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index e377d7daef0..88d292b5570 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -5,10 +5,22 @@ import * as UI from '../../ui/legacy/legacy.js'; import livematePanelStyles from './livematePanel.css.js'; +import * as SDK from '../../core/sdk/sdk.js'; +import { ReactDevToolsViewBase } from '../react_devtools/ReactDevToolsViewBase.js'; +import * as i18n from '../../core/i18n/i18n.js'; let livematePanelInstance: LivematePanel; -export class LivematePanel extends UI.Widget.VBox { +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(); @@ -16,21 +28,48 @@ export class LivematePanel extends UI.Widget.VBox { return livematePanelInstance; } - private constructor() { - super(true, true); - this.registerRequiredCSS(livematePanelStyles); - this.contentElement.classList.add('livemate-panel'); + constructor() { + super('components', i18nString(UIStrings.title)); } - override wasShown(): void { - super.wasShown(); - this.renderContent(); - } + override renderDevToolsView(): void { + this.clearView(); + + 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(); + + const button = document.createElement('button'); + button.textContent = 'Start Inspecting Host'; + + bridge.addListener('selectElement', (element: any) => {console.log(element)}); - private renderContent(): void { - this.contentElement.removeChildren(); + let inspecting = false; - const header = this.contentElement.createChild('div', 'livemate-header'); - header.textContent = 'Livemate Panel'; + button.onclick = () => { + if (inspecting) { + (bridge as any).send('stopInspectingHost'); + button.textContent= 'Start Inspecting Host'; + inspecting = false; + } else { + (bridge as any).send('startInspectingHost', false); + button.textContent= 'Stop Inspecting Host'; + inspecting = true; + } + } + + this.contentElement.appendChild(button); + + + + // bridge.send('stopInspectingHost'); } + + // this.contentElement.removeChildren(); + + // const header = this.contentElement.createChild('div', 'livemate-header'); + // header.textContent = 'Livemate Panel'; } 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(); } } From 6a3fe52707c7d1feb5fde31a93a6c93b734e64ed Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Fri, 12 Dec 2025 09:36:19 -0800 Subject: [PATCH 3/9] Test livemate shell execution --- front_end/core/host/InspectorFrontendHost.ts | 3 + .../core/host/InspectorFrontendHostAPI.ts | 8 ++ front_end/panels/livemate/LivematePanel.ts | 86 +++++++++++-------- front_end/panels/livemate/livematePanel.css | 86 +++++++++++++++++-- 4 files changed, 136 insertions(+), 47 deletions(-) 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/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index 88d292b5570..2469d78ddaa 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -1,13 +1,12 @@ -// Copyright (c) Meta Platforms, Inc. and affiliates. -// Copyright 2024 The Chromium Authors. All rights reserved. +// 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 UI from '../../ui/legacy/legacy.js'; -import livematePanelStyles from './livematePanel.css.js'; -import * as SDK from '../../core/sdk/sdk.js'; -import { ReactDevToolsViewBase } from '../react_devtools/ReactDevToolsViewBase.js'; +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; @@ -17,7 +16,10 @@ const UIStrings = { */ title: '⚛️ Livemate', } as const; -const str_ = i18n.i18n.registerUIStrings('panels/livemate/LivematePanel.ts', UIStrings); +const str_ = i18n.i18n.registerUIStrings( + 'panels/livemate/LivematePanel.ts', + UIStrings +); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class LivematePanel extends ReactDevToolsViewBase { @@ -28,48 +30,56 @@ export class LivematePanel extends ReactDevToolsViewBase { return livematePanelInstance; } - constructor() { + constructor() { super('components', i18nString(UIStrings.title)); + this.registerRequiredCSS(livematePanelStyles); } override renderDevToolsView(): void { this.clearView(); - const model = this.model; - if (model === null) { - throw new Error('Attempted to render React DevTools panel, but the model was null'); - } + this.contentElement.classList.add('livemate-panel'); - const bridge = model.getBridgeOrThrow(); + const promptSection = this.contentElement.createChild( + 'div', + 'livemate-prompt-section' + ); - const button = document.createElement('button'); - button.textContent = 'Start Inspecting Host'; + const promptTextarea = document.createElement('textarea'); + promptTextarea.className = 'livemate-prompt-input'; + promptTextarea.placeholder = 'Ask Devmate anything about this app...'; + promptSection.appendChild(promptTextarea); - bridge.addListener('selectElement', (element: any) => {console.log(element)}); + const sendButton = promptSection.createChild( + 'button', + 'livemate-send-button' + ); + sendButton.textContent = 'Send to Devmate'; - let inspecting = false; + const statusArea = promptSection.createChild('div', 'livemate-status'); - button.onclick = () => { - if (inspecting) { - (bridge as any).send('stopInspectingHost'); - button.textContent= 'Start Inspecting Host'; - inspecting = false; - } else { - (bridge as any).send('startInspectingHost', false); - button.textContent= 'Stop Inspecting Host'; - inspecting = true; + const handleSend = (): void => { + const prompt = promptTextarea.value.trim(); + if (!prompt) { + statusArea.textContent = 'Please enter a prompt'; + statusArea.className = 'livemate-status error'; + return; } - } - - this.contentElement.appendChild(button); - - - - // bridge.send('stopInspectingHost'); + statusArea.textContent = 'Sending to Devmate...'; + statusArea.className = 'livemate-status pending'; + + ( + Host.InspectorFrontendHost.InspectorFrontendHostInstance as unknown as { + sendToDevmate: (prompt: string) => void, + } + ).sendToDevmate(prompt); + }; + + sendButton.addEventListener('click', handleSend); + promptTextarea.addEventListener('keydown', e => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + handleSend(); + } + }); } - - // this.contentElement.removeChildren(); - - // const header = this.contentElement.createChild('div', 'livemate-header'); - // header.textContent = 'Livemate Panel'; } diff --git a/front_end/panels/livemate/livematePanel.css b/front_end/panels/livemate/livematePanel.css index 9e0a0eb20f6..93e99b3b03a 100644 --- a/front_end/panels/livemate/livematePanel.css +++ b/front_end/panels/livemate/livematePanel.css @@ -1,20 +1,88 @@ .livemate-panel { display: flex; flex-direction: column; - overflow: auto; - padding: 12px; + height: 100%; + padding: 16px; background-color: var(--sys-color-cdt-base-container); + box-sizing: border-box; } -.livemate-header { - font-size: 18px; - font-weight: 500; - margin-bottom: 16px; - color: var(--sys-color-on-surface); +.livemate-prompt-section { + display: flex; + flex-direction: column; + gap: 12px; + max-width: 600px; + width: 100%; + margin: 0 auto; } -.livemate-content { +.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); - line-height: 1.6; + 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); } From 6915bd73c6792c40cca4a5d5cb63f7787f92fa8b Mon Sep 17 00:00:00 2001 From: devan Date: Mon, 15 Dec 2025 22:16:16 -0800 Subject: [PATCH 4/9] Update component selector --- front_end/panels/livemate/LivematePanel.ts | 139 +++++++++++++++++---- 1 file changed, 113 insertions(+), 26 deletions(-) diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index 2469d78ddaa..da95e98fec8 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -45,41 +45,128 @@ export class LivematePanel extends ReactDevToolsViewBase { 'livemate-prompt-section' ); - const promptTextarea = document.createElement('textarea'); - promptTextarea.className = 'livemate-prompt-input'; - promptTextarea.placeholder = 'Ask Devmate anything about this app...'; - promptSection.appendChild(promptTextarea); - - const sendButton = promptSection.createChild( - 'button', - 'livemate-send-button' - ); - sendButton.textContent = 'Send to Devmate'; + // 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: Inspect button and breadcrumb + const topRow = document.createElement('div'); + topRow.setAttribute('style', 'display: flex; align-items: center; gap: 8px;'); + + // Inspect button + const inspectButton = document.createElement('button'); + inspectButton.textContent = 'Inspect'; + inspectButton.setAttribute('style', 'padding: 4px 12px; cursor: pointer;'); + let isInspecting = false; + inspectButton.addEventListener('click', () => { + isInspecting = !isInspecting; + if (isInspecting) { + (bridge as any).send('startInspectingNative'); + inspectButton.textContent = 'Stop Inspecting'; + } else { + (bridge as any).send('stopInspectingNative'); + inspectButton.textContent = 'Inspect'; + } + }); - const statusArea = promptSection.createChild('div', 'livemate-status'); + // 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;'); - const handleSend = (): void => { - const prompt = promptTextarea.value.trim(); - if (!prompt) { - statusArea.textContent = 'Please enter a prompt'; - statusArea.className = 'livemate-status error'; + // 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 = ''; + + // Function to update breadcrumb with component data + const updateBreadcrumb = (components: Array<{name: string}>): void => { + breadcrumb.innerHTML = ''; + + if (components.length === 0) { return; } - statusArea.textContent = 'Sending to Devmate...'; - statusArea.className = 'livemate-status pending'; - ( - Host.InspectorFrontendHost.InspectorFrontendHostInstance as unknown as { - sendToDevmate: (prompt: string) => void, + // 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); } - ).sendToDevmate(prompt); + }); }; - sendButton.addEventListener('click', handleSend); - promptTextarea.addEventListener('keydown', e => { - if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { - handleSend(); + // Listen for component data from React DevTools + bridge.addListener('viewDataAtPoint', (data: unknown) => { + updateBreadcrumb(data as Array<{name: string}>); + }); + + topRow.appendChild(inspectButton); + 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;'); + + // 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', () => { + const query = queryInput.value; + if (query.trim()) { + console.log('Sending to Devmate:', query); + // statusArea.textContent = 'Sending to Devmate...'; + // statusArea.className = 'livemate-status pending'; + ( + Host.InspectorFrontendHost.InspectorFrontendHostInstance as unknown as { + sendToDevmate: (prompt: string) => void, + } + ).sendToDevmate(query); } }); + + bottomRow.appendChild(queryInput); + bottomRow.appendChild(sendButton); + + toolbarContainer.appendChild(topRow); + toolbarContainer.appendChild(bottomRow); + + outerWrapper.appendChild(toolbarContainer); + this.contentElement.appendChild(outerWrapper); + + + } } From 881b6161c7c23e3492019710870b56c1672cbe51 Mon Sep 17 00:00:00 2001 From: devan Date: Wed, 17 Dec 2025 15:31:16 -0800 Subject: [PATCH 5/9] Fix bridge --- front_end/panels/livemate/LivematePanel.ts | 70 ++++++++++++---------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index da95e98fec8..bbf7c979f2a 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -40,10 +40,12 @@ export class LivematePanel extends ReactDevToolsViewBase { this.contentElement.classList.add('livemate-panel'); - const promptSection = this.contentElement.createChild( - 'div', - 'livemate-prompt-section' - ); + 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'); @@ -53,26 +55,10 @@ export class LivematePanel extends ReactDevToolsViewBase { 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: Inspect button and breadcrumb + // First row: breadcrumb const topRow = document.createElement('div'); topRow.setAttribute('style', 'display: flex; align-items: center; gap: 8px;'); - // Inspect button - const inspectButton = document.createElement('button'); - inspectButton.textContent = 'Inspect'; - inspectButton.setAttribute('style', 'padding: 4px 12px; cursor: pointer;'); - let isInspecting = false; - inspectButton.addEventListener('click', () => { - isInspecting = !isInspecting; - if (isInspecting) { - (bridge as any).send('startInspectingNative'); - inspectButton.textContent = 'Stop Inspecting'; - } else { - (bridge as any).send('stopInspectingNative'); - inspectButton.textContent = 'Inspect'; - } - }); - // 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;'); @@ -82,6 +68,9 @@ export class LivematePanel extends ReactDevToolsViewBase { 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 = ''; @@ -123,10 +112,10 @@ export class LivematePanel extends ReactDevToolsViewBase { // Listen for component data from React DevTools bridge.addListener('viewDataAtPoint', (data: unknown) => { - updateBreadcrumb(data as Array<{name: string}>); + currentHierarchy = data as Array<{name: string}>; + updateBreadcrumb(currentHierarchy); }); - topRow.appendChild(inspectButton); topRow.appendChild(breadcrumb); topRow.appendChild(selectedComponentBox); @@ -139,24 +128,41 @@ export class LivematePanel extends ReactDevToolsViewBase { 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;'); - // 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', () => { + // Function to send query to Devmate + const sendQueryToDevmate = (): void => { const query = queryInput.value; if (query.trim()) { - console.log('Sending to Devmate:', query); - // statusArea.textContent = 'Sending to Devmate...'; - // statusArea.className = 'livemate-status pending'; + // 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(query); + ).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); From fcb03032e9ceeae344f8bbdfe69751c3cdff315a Mon Sep 17 00:00:00 2001 From: devan Date: Fri, 19 Dec 2025 10:56:45 -0800 Subject: [PATCH 6/9] More UI changes --- front_end/panels/livemate/LivematePanel.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index bbf7c979f2a..4497c7ea39a 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -55,10 +55,19 @@ export class LivematePanel extends ReactDevToolsViewBase { 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 + // 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('startInspector'); + }); + 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;'); @@ -80,10 +89,10 @@ export class LivematePanel extends ReactDevToolsViewBase { } // Set the selected component to the first one (most specific) - selectedComponentBox.textContent = components[0].name; + // 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); + const breadcrumbComponents = components.slice(-5); breadcrumbComponents.forEach((component, index) => { const componentSpan = document.createElement('span'); From d06ab338d89c5ceff6a0b9076a4d24356854bbec Mon Sep 17 00:00:00 2001 From: devan Date: Mon, 19 Jan 2026 21:40:55 -0800 Subject: [PATCH 7/9] Panel changes --- front_end/panels/livemate/LivematePanel.ts | 32 ++++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index 4497c7ea39a..3e22448ad7b 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -64,7 +64,7 @@ export class LivematePanel extends ReactDevToolsViewBase { pickComponentButton.textContent = 'Pick component'; pickComponentButton.setAttribute('style', 'padding: 4px 12px; cursor: pointer;'); pickComponentButton.addEventListener('click', () => { - (bridge as unknown as {send: (event: string) => void}).send('startInspector'); + (bridge as unknown as {send: (event: string) => void}).send('startInspectingHost'); }); topRow.appendChild(pickComponentButton); @@ -73,9 +73,9 @@ export class LivematePanel extends ReactDevToolsViewBase { 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 = ''; + // 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}> = []; @@ -98,9 +98,6 @@ export class LivematePanel extends ReactDevToolsViewBase { 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'; }); @@ -126,7 +123,6 @@ export class LivematePanel extends ReactDevToolsViewBase { }); topRow.appendChild(breadcrumb); - topRow.appendChild(selectedComponentBox); // Second row: AI query input and send button const bottomRow = document.createElement('div'); @@ -154,7 +150,25 @@ export class LivematePanel extends ReactDevToolsViewBase { sendToDevmate: (prompt: string) => void, } ).sendToDevmate(prompt); - queryInput.value = ''; + + // Disable input and button with grayed out appearance + queryInput.disabled = true; + queryInput.style.opacity = '0.5'; + queryInput.style.cursor = 'not-allowed'; + sendButton.disabled = true; + sendButton.style.opacity = '0.5'; + sendButton.style.cursor = 'not-allowed'; + + // Re-enable and clear after 3 seconds + setTimeout(() => { + queryInput.value = ''; + queryInput.disabled = false; + queryInput.style.opacity = '1'; + queryInput.style.cursor = 'text'; + sendButton.disabled = false; + sendButton.style.opacity = '1'; + sendButton.style.cursor = 'pointer'; + }, 3000); } }; From de7a5e566fb9259fd238c1af7c7b27e882a61684 Mon Sep 17 00:00:00 2001 From: devan Date: Mon, 19 Jan 2026 23:12:25 -0800 Subject: [PATCH 8/9] Reset front_end/panels/react_devtools/ReactDevToolsViewBase.ts --- .../react_devtools/ReactDevToolsViewBase.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/front_end/panels/react_devtools/ReactDevToolsViewBase.ts b/front_end/panels/react_devtools/ReactDevToolsViewBase.ts index aaf2caf625e..4f6eee89c63 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(); } } From 332d3a1658438adf15d24392370f37340b7d76cf Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes <57368278+jorge-cab@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:36:38 -0800 Subject: [PATCH 9/9] Send prompt data with http endpoint instead of through the electron shell --- front_end/panels/livemate/LivematePanel.ts | 108 ++++++++++++--------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/front_end/panels/livemate/LivematePanel.ts b/front_end/panels/livemate/LivematePanel.ts index 3e22448ad7b..80b7b2c5f1a 100644 --- a/front_end/panels/livemate/LivematePanel.ts +++ b/front_end/panels/livemate/LivematePanel.ts @@ -2,7 +2,6 @@ // 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'; @@ -129,54 +128,15 @@ export class LivematePanel extends ReactDevToolsViewBase { bottomRow.setAttribute('style', 'display: flex; align-items: center; gap: 8px;'); // AI query text box - const queryInput = document.createElement('textarea'); + 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;'); - // 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); - - // Disable input and button with grayed out appearance - queryInput.disabled = true; - queryInput.style.opacity = '0.5'; - queryInput.style.cursor = 'not-allowed'; - sendButton.disabled = true; - sendButton.style.opacity = '0.5'; - sendButton.style.cursor = 'not-allowed'; - - // Re-enable and clear after 3 seconds - setTimeout(() => { - queryInput.value = ''; - queryInput.disabled = false; - queryInput.style.opacity = '1'; - queryInput.style.cursor = 'text'; - sendButton.disabled = false; - sendButton.style.opacity = '1'; - sendButton.style.cursor = 'pointer'; - }, 3000); - } - }; - // Handle Enter key to send prompt (Shift+Enter for newline) - queryInput.addEventListener('keydown', (event: KeyboardEvent) => { + queryInput.addEventListener('keydown', async (event: KeyboardEvent) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); - sendQueryToDevmate(); + await sendCommandToMetro(queryInput, currentHierarchy); } }); @@ -184,7 +144,9 @@ export class LivematePanel extends ReactDevToolsViewBase { 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); + sendButton.addEventListener('click', async () => { + await sendCommandToMetro(queryInput, currentHierarchy); + }); bottomRow.appendChild(queryInput); bottomRow.appendChild(sendButton); @@ -194,8 +156,66 @@ export class LivematePanel extends ReactDevToolsViewBase { 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', + }; } }