Skip to content

Commit 6bb2565

Browse files
antfucoderabbitai[bot]atinux
authored
feat: enhance inspect panel, add copy visual info for agents (#928)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Sébastien Chopin <atinux@gmail.com>
1 parent be4c647 commit 6bb2565

File tree

5 files changed

+280
-612
lines changed

5 files changed

+280
-612
lines changed

packages/devtools-ui-kit/src/module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default defineNuxtModule<ModuleOptions>({
3232

3333
nuxt.options.css.unshift(rPath('assets/styles.css'))
3434

35-
if (!options.dev)
35+
if (!options.dev && nuxt.options.unocss)
3636
nuxt.options.unocss = extendUnocssOptions(nuxt.options.unocss)
3737

3838
// eslint-disable-next-line ts/ban-ts-comment

packages/devtools/src/module-main.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type {} from '@vitejs/devtools-kit'
22
import type { ServerResponse } from 'node:http'
33
import type { Nuxt } from 'nuxt/schema'
4-
import type { ViteDevServer } from 'vite'
54
import type { ModuleOptions, NuxtDevToolsOptions } from './types'
65
import { existsSync } from 'node:fs'
76
import fs from 'node:fs/promises'
@@ -168,7 +167,7 @@ window.__NUXT_DEVTOOLS_TIME_METRIC__.appInit = Date.now()
168167
const ROUTE_ANALYZE = `${ROUTE_PATH}/analyze`
169168

170169
// TODO: Use WS from nitro server when possible
171-
nuxt.hook('vite:serverCreated', (server: ViteDevServer) => {
170+
nuxt.hook('vite:serverCreated', (server) => {
172171
const devtoolsAnalyzeDir = join(nuxt.options.rootDir, 'node_modules/.cache/nuxt-devtools/analyze')
173172

174173
server.middlewares.use(ROUTE_ANALYZE, sirv(devtoolsAnalyzeDir, { single: false, dev: true, dotfiles: true, ignores: false }))

packages/devtools/src/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/devtools/src/webcomponents/components/NuxtDevtoolsInspectPanel.vue

Lines changed: 201 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ const PANEL_WIDTH = 400
1414
const PANEL_HEIGHT = 300
1515
const PANEL_MARGIN = 30
1616
17+
const showToast = ref(false)
18+
const toastContent = ref('')
19+
let toastTimer: ReturnType<typeof setTimeout> | undefined
20+
1721
const initX = ref(0)
1822
const initY = ref(0)
1923
@@ -73,10 +77,149 @@ async function openInEditor() {
7377
emit('openInEditor', file)
7478
// close()
7579
}
80+
81+
function generateUniqueSelector(element: Element | undefined): string {
82+
if (!element)
83+
return ''
84+
85+
const path: string[] = []
86+
let current: Element | null = element
87+
88+
while (current && current !== document.body) {
89+
let selector = current.tagName.toLowerCase()
90+
91+
// Add ID if available
92+
if (current.id) {
93+
selector += `#${CSS.escape(current.id)}`
94+
path.unshift(selector)
95+
break // ID is unique, no need to go further
96+
}
97+
98+
// Add classes
99+
if (current.className && typeof current.className === 'string') {
100+
const classes = current.className.trim().split(/\s+/).filter(Boolean)
101+
if (classes.length > 0)
102+
selector += `.${classes.map(c => CSS.escape(c)).join('.')}`
103+
}
104+
105+
// Add nth-child if there are siblings of the same type
106+
const parent = current.parentElement
107+
if (parent) {
108+
const siblings = Array.from(parent.children).filter(
109+
child => child.tagName === current!.tagName,
110+
)
111+
if (siblings.length > 1) {
112+
const index = siblings.indexOf(current) + 1
113+
selector += `:nth-of-type(${index})`
114+
}
115+
}
116+
117+
path.unshift(selector)
118+
current = current.parentElement
119+
}
120+
121+
if (path.length > 6)
122+
return ''
123+
124+
return path.join(' > ')
125+
}
126+
127+
function buildComponentTree(): string {
128+
if (!props.matched)
129+
return ''
130+
131+
const components: string[] = []
132+
let current: typeof props.matched | undefined = props.matched
133+
134+
// Build the tree by walking up the parent chain
135+
while (current) {
136+
// Try to get component name from vnode
137+
let componentName = 'Unknown'
138+
if (current.vnode) {
139+
const vnode = current.vnode
140+
if (vnode.type) {
141+
if (typeof vnode.type === 'string') {
142+
componentName = vnode.type
143+
}
144+
else if (typeof vnode.type === 'object') {
145+
const typeObj = vnode.type as any
146+
componentName = typeObj.name || typeObj.__name || typeObj.__file?.split('/').pop()?.replace(/\.\w+$/, '') || 'AnonymousComponent'
147+
}
148+
else if (typeof vnode.type === 'function') {
149+
componentName = (vnode.type as any).name || 'FunctionalComponent'
150+
}
151+
}
152+
}
153+
154+
components.unshift(componentName)
155+
156+
// Move to parent
157+
try {
158+
current = current.getParent()
159+
}
160+
catch {
161+
break
162+
}
163+
}
164+
165+
return components.join(' > ')
166+
}
167+
168+
async function copyAgentInfo() {
169+
if (!props.matched)
170+
return
171+
172+
try {
173+
// Gather browser context
174+
const pageUrl = window.location.href
175+
const viewport = `${window.innerWidth}x${window.innerHeight}`
176+
const selectedText = window.getSelection()?.toString()?.trim()
177+
178+
// Extract element information
179+
const filepath = props.matched.pos[0]
180+
const line = props.matched.pos[1]
181+
const column = props.matched.pos[2]
182+
const fileLocation = `${filepath}:${line}:${column}`
183+
184+
// Generate CSS selector
185+
const selector = generateUniqueSelector(props.matched.el)
186+
187+
// Build component tree
188+
const componentTree = buildComponentTree()
189+
190+
// Format the output
191+
const info = [
192+
`Page URL: ${pageUrl}`,
193+
`Viewport: ${viewport}`,
194+
selectedText ? `Selected Text: ${selectedText}` : null,
195+
selector ? `Selector: ${selector}` : null,
196+
componentTree ? `Component Tree: ${componentTree}` : null,
197+
`File: ${fileLocation}`,
198+
].filter(Boolean).join('\n')
199+
200+
// Copy to clipboard
201+
await navigator.clipboard.writeText(info)
202+
203+
// Show toast with preview
204+
toastContent.value = info
205+
showToast.value = true
206+
207+
if (toastTimer)
208+
clearTimeout(toastTimer)
209+
// Hide toast after 6 seconds
210+
toastTimer = setTimeout(() => {
211+
showToast.value = false
212+
}, 6_000)
213+
}
214+
catch (error) {
215+
console.error('Failed to copy agent info:', error)
216+
}
217+
}
76218
</script>
77219

78220
<template>
79221
<div
222+
v-if="props.matched"
80223
ref="el"
81224
class="fixed relative z-9999999 w-400px flex flex-col of-hidden rounded-lg bg-glass text-sm color-base shadow-lg ring-1 ring-base backdrop-blur duration-200"
82225
:style="style"
@@ -90,31 +233,64 @@ async function openInEditor() {
90233
>
91234
<div class="nuxt-devtools-inspect-running-border pointer-events-none absolute inset-0 z-10 border-1.5 border-transparent rounded-lg" />
92235
</div> -->
93-
<div ref="draggingEl" class="flex items-center gap-2 p2">
94-
<button
95-
title="Go to parent"
96-
class="flex items-center text-sm font-mono op50 disabled:pointer-events-none hover:text-green6 hover:op100 disabled:op10!"
97-
:disabled="!props.hasParent"
98-
@click="selectParent"
99-
>
100-
<div class="i-ph-arrow-bend-left-up-duotone text-lg" />
101-
</button>
102-
<button
103-
title="Open in editor"
104-
class="flex items-center text-sm font-mono op50 hover:text-green6 hover:op100"
105-
@click="openInEditor"
106-
>
107-
<span v-if="props.matched">{{ props.matched.pos[0] }}:{{ props.matched.pos[1] }}:{{ props.matched.pos[2] }}</span>
108-
<div class="i-ph-arrow-up-right-light mt--2 text-lg" />
109-
</button>
110-
<div class="flex-auto" />
111-
<button
112-
title="Close"
113-
class="flex-none op50 hover:op100"
114-
@click="close"
115-
>
116-
<div class="i-ph-x text-lg" />
117-
</button>
236+
<div ref="draggingEl" class="flex flex-col gap-2 of-hidden p2">
237+
<div class="flex items-center gap-2">
238+
<button
239+
v-if="props.hasParent"
240+
title="Go to parent"
241+
class="flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 disabled:pointer-events-none hover:text-green6 hover:op100 disabled:op10!"
242+
@click="selectParent"
243+
>
244+
<div class="i-ph-arrow-bend-left-up-duotone text-lg" />
245+
Parent
246+
</button>
247+
<button
248+
title="Open in editor"
249+
class="flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 hover:text-green6 hover:op100"
250+
@click="openInEditor"
251+
>
252+
<div class="i-ph-arrow-up-right-light text-lg" />
253+
Open
254+
</button>
255+
<button
256+
title="Copy infos for agents"
257+
class="flex items-center border-1 border-base rounded px1 py0.5 text-sm font-mono op50 hover:text-green6 hover:op100"
258+
@click="copyAgentInfo"
259+
>
260+
<div class="i-ph-copy-duotone text-lg" />
261+
Info
262+
</button>
263+
<div class="flex-auto" />
264+
<button
265+
title="Close"
266+
class="flex-none op50 hover:op100"
267+
@click="close"
268+
>
269+
<div class="i-ph-x text-lg" />
270+
</button>
271+
</div>
272+
<div class="grid grid-cols-[max-content_1fr] items-center gap-2">
273+
<!-- File -->
274+
<div class="i-ph-file-duotone flex-none text-lg op50" title="File" />
275+
<span class="break-all text-xs font-mono" title="File location">{{ props.matched.pos[0] }}:{{ props.matched.pos[1] }}:{{ props.matched.pos[2] }}</span>
276+
277+
<!-- Component Tree -->
278+
<div class="i-ph-tree-view-duotone flex-none text-lg op50" title="Component Tree" />
279+
<span class="break-all text-xs font-mono" title="Component Tree">{{ buildComponentTree() }}</span>
280+
</div>
281+
</div>
282+
</div>
283+
284+
<!-- Toast notification -->
285+
<div
286+
v-show="showToast"
287+
class="fixed bottom-20px right-20px z-99999999 max-w-500px flex flex-col gap-2 rounded-lg bg-glass p4 text-sm color-base shadow-xl ring-1 ring-base backdrop-blur transition-all duration-300"
288+
:class="showToast ? 'translate-y-0 op100' : 'translate-y-10 op0'"
289+
>
290+
<div class="flex items-center gap-2">
291+
<div class="i-ph-check-circle-duotone text-xl text-green6" />
292+
<span class="font-semibold">Infos for agents copied to clipboard</span>
118293
</div>
294+
<pre class="mt-2 max-h-200px of-auto whitespace-pre-wrap rounded bg-black bg-op-20 p2 text-xs font-mono">{{ toastContent }}</pre>
119295
</div>
120296
</template>

0 commit comments

Comments
 (0)