feat: wire CMD-K spotlight search to installed apps
Dynamically builds searchable items from installed packages so typing an app name in CMD-K finds and launches it via the app launcher overlay. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
064da257da
commit
70bc71d035
@ -59,7 +59,7 @@ After getting Claude Max OAuth working on the live server, hardening the deploy
|
||||
|
||||
## Phase 2: Service Wiring & Search (Tasks 10-16) — ~2.5 hours
|
||||
|
||||
### Task 10: CMD-K app search and launch
|
||||
### Task 10: CMD-K app search and launch [DONE]
|
||||
- **Files**: `neode-ui/src/components/SpotlightSearch.vue`
|
||||
- **Change**: Import `useAppStore` and `useAppLauncherStore`. Create computed `dynamicAppItems` from `store.packages`. Merge with static help tree items in Fuse search. When app item selected, call `launchApp()`.
|
||||
- **Verify**: CMD+K, type app name, appears in results, click to launch
|
||||
|
||||
@ -115,11 +115,15 @@ import { useRouter } from 'vue-router'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useSpotlightStore } from '@/stores/spotlight'
|
||||
import { useCLIStore } from '@/stores/cli'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { helpTree, flattenForSearch, type SearchableItem } from '@/data/helpTree'
|
||||
|
||||
const router = useRouter()
|
||||
const spotlightStore = useSpotlightStore()
|
||||
const cliStore = useCLIStore()
|
||||
const appStore = useAppStore()
|
||||
const appLauncherStore = useAppLauncherStore()
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
@ -129,16 +133,31 @@ const query = ref('')
|
||||
const isDragging = ref(false)
|
||||
const dragStart = ref<{ x: number; y: number; panelX: number; panelY: number } | null>(null)
|
||||
|
||||
const searchableItems = flattenForSearch()
|
||||
const fuse = new Fuse(searchableItems, {
|
||||
const staticItems = flattenForSearch()
|
||||
|
||||
// Build dynamic app items from installed packages
|
||||
const dynamicAppItems = computed<SearchableItem[]>(() => {
|
||||
const pkgs = appStore.packages
|
||||
return Object.entries(pkgs).map(([id, pkg]) => ({
|
||||
id: `app-${id}`,
|
||||
label: pkg.manifest?.title || id,
|
||||
path: `__launch_app__:${id}`,
|
||||
type: 'action' as const,
|
||||
section: 'Installed Apps',
|
||||
}))
|
||||
})
|
||||
|
||||
const allSearchableItems = computed(() => [...staticItems, ...dynamicAppItems.value])
|
||||
|
||||
const fuse = computed(() => new Fuse(allSearchableItems.value, {
|
||||
keys: ['label', 'section'],
|
||||
threshold: 0.4,
|
||||
})
|
||||
}))
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
const q = query.value.trim()
|
||||
if (!q) return []
|
||||
const results = fuse.search(q)
|
||||
const results = fuse.value.search(q)
|
||||
return results.map((r) => r.item)
|
||||
})
|
||||
|
||||
@ -148,7 +167,7 @@ const recentOffset = computed(() =>
|
||||
|
||||
const selectableCount = computed(() => {
|
||||
if (query.value.trim()) return filteredItems.value.length
|
||||
return recentOffset.value + searchableItems.length
|
||||
return recentOffset.value + allSearchableItems.value.length
|
||||
})
|
||||
|
||||
const panelStyle = computed(() => {
|
||||
@ -202,6 +221,20 @@ function getItemClass(index: number) {
|
||||
: 'hover:bg-white/10 text-white/90'
|
||||
}
|
||||
|
||||
function launchInstalledApp(appId: string) {
|
||||
const pkg = appStore.packages[appId]
|
||||
if (!pkg) return
|
||||
let lanAddress = pkg.installed?.['interface-addresses']?.main?.['lan-address']
|
||||
if (lanAddress && lanAddress.includes('localhost')) {
|
||||
lanAddress = lanAddress.replace('localhost', window.location.hostname)
|
||||
}
|
||||
if (lanAddress) {
|
||||
appLauncherStore.open({ url: lanAddress, title: pkg.manifest?.title || appId })
|
||||
} else {
|
||||
router.push(`/dashboard/apps/${appId}`).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
function selectItem(item: SearchableItem) {
|
||||
spotlightStore.addRecentItem({
|
||||
id: item.id,
|
||||
@ -210,7 +243,9 @@ function selectItem(item: SearchableItem) {
|
||||
type: item.type,
|
||||
})
|
||||
spotlightStore.close()
|
||||
if (item.path === '__cli__') {
|
||||
if (item.path?.startsWith('__launch_app__:')) {
|
||||
launchInstalledApp(item.path.replace('__launch_app__:', ''))
|
||||
} else if (item.path === '__cli__') {
|
||||
cliStore.open()
|
||||
} else if (item.path) {
|
||||
router.push(item.path)
|
||||
@ -228,7 +263,9 @@ function selectHelpItem(section: { id: string }, item: { id: string; label: stri
|
||||
type,
|
||||
})
|
||||
spotlightStore.close()
|
||||
if (item.path === '__cli__') {
|
||||
if (item.path?.startsWith('__launch_app__:')) {
|
||||
launchInstalledApp(item.path.replace('__launch_app__:', ''))
|
||||
} else if (item.path === '__cli__') {
|
||||
cliStore.open()
|
||||
} else if (item.path) {
|
||||
router.push(item.path)
|
||||
@ -239,6 +276,10 @@ function selectHelpItem(section: { id: string }, item: { id: string; label: stri
|
||||
|
||||
function selectRecent(item: { id: string; label: string; path?: string; type: 'navigate' | 'learn' | 'action' | 'goal' }) {
|
||||
spotlightStore.close()
|
||||
if (item.path?.startsWith('__launch_app__:')) {
|
||||
launchInstalledApp(item.path.replace('__launch_app__:', ''))
|
||||
return
|
||||
}
|
||||
if (item.path === '__cli__') {
|
||||
cliStore.open()
|
||||
return
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user