Skip to content
Merged
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
2 changes: 1 addition & 1 deletion app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ provide('navigation', versionNavigation)
</script>

<template>
<UApp :tooltip="{ delayDuration: 500 }">
<UApp :tooltip="{ delayDuration: 300 }">
<NuxtLoadingIndicator color="var(--ui-primary)" />

<div class="flex">
Expand Down
75 changes: 75 additions & 0 deletions app/components/module/ModuleInstallGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
import type { Module } from '#shared/types'

const props = withDefaults(
defineProps<{
module: Pick<Module, 'name' | 'npm' | 'description' | 'website' | 'repo'>
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
label?: string
}>(),
{
size: 'md',
label: 'Install this module'
}
)

const { installCommand, copyInstall, copyPrompt, openCursor, openClaude, openVSCode } = useModuleInstallActions(() => props.module, 'install-group')

const items = computed<DropdownMenuItem[][]>(() => [
[
{
label: 'Copy install command',
icon: 'i-lucide-terminal',
onSelect: copyInstall
},
{
label: 'Copy agent prompt',
icon: 'i-custom-ai',
onSelect: copyPrompt
}
],
[
{
label: 'Open in Cursor',
icon: 'i-simple-icons-cursor',
onSelect: openCursor
},
{
label: 'Open in Claude Code',
icon: 'i-simple-icons-anthropic',
onSelect: openClaude
},
{
label: 'Open in VS Code',
icon: 'i-simple-icons-visualstudiocode',
onSelect: openVSCode
}
Comment on lines +33 to +47

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could easily get into a state where we have so many IDEs listed here.... why these two, for example?

maybe skip it and just have install + prompt options?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I listed these two because I think they're the most widely used, but also because they're the only ones I know of at the moment that have this deeplink system, which is very convenient

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode://GitHub.Copilot-Chat/chat?agent=agent&prompt=<URL-encoded-prompt>
vscode-insiders://GitHub.Copilot-Chat/chat?agent=agent&prompt=<URL-encoded-prompt>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added VS Code, but I don't know if having Insider as well is redundant, which confirms your point that it's complicated to list some and not others, and at the same time it's a feature I find really cool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with the GitHub Copilot app, you can do something like ghapp://model/gpt-5.5 if you want to open the app with that model, or something like these:

ghapp://session/new?repo=github/github-app
ghapp://session/new/github/github-app
ghapp://session/new?repo=github/github-app&pr=123
ghapp://session/new?repo=github/github-app&branch=main
ghapp://session/new?repo=github/github-app&prompt=fix+bug
ghapp://session/new?repo=github/github-app&mode=plan
ghapp://session/new?repo=github/github-app&mode=interactive
ghapp://session/new?repo=github/github-app&mode=autopilot
ghapp://session/new?repo=github/github-app&pr=123&prompt=review+this+PR&mode=autopilot
ghapp://session/new?repo=github/github-app&branch=feature/name&prompt=continue+work&mode=plan

]
])
</script>

<template>
<UFieldGroup :size="size">
<UTooltip :text="`Copy: ${installCommand}`" class="grow">
<UButton
color="neutral"
variant="solid"
icon="i-lucide-terminal"
:label="label"
block
@click="copyInstall"
/>
</UTooltip>

<UDropdownMenu :items="items" :content="{ align: 'end' }">
<UButton
color="neutral"
variant="solid"
icon="i-lucide-chevron-down"
aria-label="More install options"
class="shrink-0"
/>
</UDropdownMenu>
</UFieldGroup>
</template>
48 changes: 37 additions & 11 deletions app/components/module/ModuleItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Module } from '#shared/types'
const emit = defineEmits<{
add: [module: Module]
remove: [module: Module]
select: [module: Module, event: MouseEvent]
}>()

const props = withDefaults(
Expand All @@ -12,17 +13,25 @@ const props = withDefaults(
showBadge?: boolean
isAdded: boolean
showAddButton?: boolean
selectable?: boolean
sortKey?: string
}>(),
{
showBadge: true,
showAddButton: true,
selectable: false,
sortKey: 'downloads'
}
)

const { copy } = useClipboard()
const { track } = useAnalytics()
const {
copyInstall: copyInstallCommand,
copyPrompt: copyAgentPrompt,
openCursor: openPromptInCursor,
openClaude: openPromptInClaudeCode,
openVSCode: openPromptInVSCode
} = useModuleInstallActions(() => props.module, 'context-menu')

const publishedAgo = useTimeAgo(() => props.module.stats.publishedAt)
const createdAgo = useTimeAgo(() => props.module.stats.createdAt)
Expand All @@ -37,12 +46,6 @@ const staticModuleDate = computed(() =>
: formatDateByLocale('en', props.module.stats.createdAt)
)

function copyInstallCommand(moduleName: string) {
track('Module Install Command Copied', { module: moduleName })
const command = `npx nuxt@latest module add ${moduleName}`
copy(command, { title: 'Command copied to clipboard:', description: command })
}

function toggleModule(m: Module) {
const action = props.isAdded ? 'remove' : 'add'
track('Module Toggled', { module: m.name, action })
Expand All @@ -54,9 +57,10 @@ function toggleModule(m: Module) {
}

function handleCardClick(event: MouseEvent) {
if (event.shiftKey) {
if (!props.selectable) return
if (event.shiftKey || event.metaKey || event.ctrlKey) {
event.preventDefault()
toggleModule(props.module)
emit('select', props.module, event)
}
}

Expand All @@ -70,7 +74,29 @@ const items = computed(() => [
{
label: 'Copy install command',
icon: 'i-lucide-terminal',
onSelect: () => copyInstallCommand(props.module.name)
onSelect: copyInstallCommand
},
{
label: 'Copy agent prompt',
icon: 'i-custom-ai',
onSelect: copyAgentPrompt
}
],
[
{
label: 'Open in Cursor',
icon: 'i-simple-icons-cursor',
onSelect: openPromptInCursor
},
{
label: 'Open in Claude Code',
icon: 'i-simple-icons-anthropic',
onSelect: openPromptInClaudeCode
},
{
label: 'Open in VS Code',
icon: 'i-simple-icons-visualstudiocode',
onSelect: openPromptInVSCode
}
],
[
Expand Down Expand Up @@ -237,7 +263,7 @@ const items = computed(() => [
color="neutral"
size="xs"
variant="outline"
@click="copyInstallCommand(module.name)"
@click="copyInstallCommand"
>
<span class="sr-only">Copy command to install {{ module.name }}</span>
</UButton>
Expand Down
29 changes: 7 additions & 22 deletions app/components/tools/ModuleCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,19 @@ const props = defineProps<{
stars?: number
}>()

const { copy } = useClipboard()
const { track } = useAnalytics()

const displayTitle = computed(() => {
const fromNpm = props.npm?.trim()
if (fromNpm) return fromNpm
return props.slug?.trim() || 'Module'
})

const installCommand = computed(() => `npx nuxt@latest module add ${props.slug}`)

const agentPrompt = computed(() => {
const lines = [`Install and configure the Nuxt module **${props.npm || props.slug}**: ${props.description || ''}`]
if (props.website) lines.push(`Docs: ${props.website}`)
if (props.repo) lines.push(`GitHub: https://github.com/${props.repo}`)
lines.push(`\nSteps:\n1. Run \`${installCommand.value}\`\n2. Read the module documentation and add recommended configuration in \`nuxt.config.ts\`\n3. List any required environment variables in \`.env.example\` without filling in actual values`)
return lines.join('\n')
})

function copyInstall() {
track('Module Install Command Copied', { module: props.slug, source: 'nuxt-agent' })
copy(installCommand.value, { title: 'Command copied to clipboard:', description: installCommand.value })
}

function copyPrompt() {
track('Module Agent Prompt Copied', { module: props.slug, source: 'nuxt-agent' })
copy(agentPrompt.value, { title: 'Agent prompt copied!', description: props.npm || props.slug, icon: 'i-custom-ai' })
}
const { copyInstall, copyPrompt } = useModuleInstallActions(() => ({
name: props.slug,
npm: props.npm || '',
description: props.description || '',
website: props.website || '',
repo: props.repo || ''
}), 'nuxt-agent')
</script>

<template>
Expand Down
87 changes: 33 additions & 54 deletions app/components/tools/PromptCard.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
<script setup lang="ts">
import {
buildClaudeCodeBrowserUrl,
buildCursorBrowserUrl
} from '../../../layers/nuxi/shared/utils/ide-deeplinks'

const props = defineProps<{
description: string
prompt: string
repo?: string
icon?: string
deeplinks: {
cursor: string
claude: string
}
}>()

const { copied } = useClipboard()
const toast = useToast()
const { copied, copy } = useClipboard()
const { track } = useAnalytics()
const {
openInCursor: openPromptInCursor,
openInClaudeCode: openPromptInClaudeCode,
openInVSCode: openPromptInVSCode
} = useIdeDeeplink()
const expanded = ref(false)

const previewLimit = 200
Expand All @@ -27,57 +21,24 @@ const previewText = computed(() => {
return `${props.prompt.slice(0, previewLimit)}…`
})

const pasteShortcut = computed(() =>
typeof navigator !== 'undefined' && navigator.platform?.includes('Mac') ? '⌘V' : 'Ctrl+V'
)

function openIdeDeeplink(url: string) {
window.location.assign(url)
}

async function copyPromptToClipboard(options: { title: string, description: string, icon: string }) {
try {
await navigator.clipboard.writeText(props.prompt)
toast.add(options)
} catch {
// Clipboard can fail without blocking the deeplink handoff.
}
}

async function openInCursor() {
function openInCursor() {
track('Nuxi Prompt Opened', { ide: 'cursor', source: 'nuxt-agent' })

const { url, needsClipboardFallback } = buildCursorBrowserUrl(props.prompt)
if (needsClipboardFallback) {
await copyPromptToClipboard({
title: 'Prompt copied',
description: `Paste with ${pasteShortcut.value} when your IDE opens`,
icon: 'i-lucide-clipboard-check'
})
}

openIdeDeeplink(url)
openPromptInCursor(props.prompt)
}

async function openInClaudeCode() {
function openInClaudeCode() {
track('Nuxi Prompt Opened', { ide: 'claude', source: 'nuxt-agent' })
openPromptInClaudeCode(props.prompt)
}

const { url, needsClipboardFallback } = buildClaudeCodeBrowserUrl(props.prompt, props.repo)

if (needsClipboardFallback) {
await copyPromptToClipboard({
title: 'Prompt copied',
description: `Paste with ${pasteShortcut.value} when your IDE opens`,
icon: 'i-lucide-clipboard-check'
})
}

openIdeDeeplink(url)
function openInVSCode() {
track('Nuxi Prompt Opened', { ide: 'vscode', source: 'nuxt-agent' })
openPromptInVSCode(props.prompt)
}

function copyPrompt() {
track('Nuxi Prompt Copied', { source: 'nuxt-agent' })
void copyPromptToClipboard({
copy(props.prompt, {
title: 'Prompt copied!',
description: props.description,
icon: 'i-lucide-clipboard-check'
Expand Down Expand Up @@ -152,6 +113,24 @@ function copyPrompt() {
</span>
</UTooltip>

<UTooltip
arrow
:delay-duration="700"
:content="{ side: 'top', sideOffset: 6 }"
text="Open prompt in VS Code (Copilot Chat)"
>
<span class="inline-flex">
<UButton
size="xs"
color="neutral"
variant="outline"
icon="i-simple-icons-visualstudiocode"
label="Open in VS Code"
@click="openInVSCode"
/>
</span>
</UTooltip>

<UTooltip
arrow
:delay-duration="700"
Expand Down
Loading