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
|
## 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`
|
- **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()`.
|
- **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
|
- **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 Fuse from 'fuse.js'
|
||||||
import { useSpotlightStore } from '@/stores/spotlight'
|
import { useSpotlightStore } from '@/stores/spotlight'
|
||||||
import { useCLIStore } from '@/stores/cli'
|
import { useCLIStore } from '@/stores/cli'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
import { helpTree, flattenForSearch, type SearchableItem } from '@/data/helpTree'
|
import { helpTree, flattenForSearch, type SearchableItem } from '@/data/helpTree'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const spotlightStore = useSpotlightStore()
|
const spotlightStore = useSpotlightStore()
|
||||||
const cliStore = useCLIStore()
|
const cliStore = useCLIStore()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
const appLauncherStore = useAppLauncherStore()
|
||||||
|
|
||||||
const inputRef = ref<HTMLInputElement | null>(null)
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
const panelRef = ref<HTMLElement | null>(null)
|
const panelRef = ref<HTMLElement | null>(null)
|
||||||
@ -129,16 +133,31 @@ const query = ref('')
|
|||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragStart = ref<{ x: number; y: number; panelX: number; panelY: number } | null>(null)
|
const dragStart = ref<{ x: number; y: number; panelX: number; panelY: number } | null>(null)
|
||||||
|
|
||||||
const searchableItems = flattenForSearch()
|
const staticItems = flattenForSearch()
|
||||||
const fuse = new Fuse(searchableItems, {
|
|
||||||
|
// 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'],
|
keys: ['label', 'section'],
|
||||||
threshold: 0.4,
|
threshold: 0.4,
|
||||||
})
|
}))
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
const q = query.value.trim()
|
const q = query.value.trim()
|
||||||
if (!q) return []
|
if (!q) return []
|
||||||
const results = fuse.search(q)
|
const results = fuse.value.search(q)
|
||||||
return results.map((r) => r.item)
|
return results.map((r) => r.item)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -148,7 +167,7 @@ const recentOffset = computed(() =>
|
|||||||
|
|
||||||
const selectableCount = computed(() => {
|
const selectableCount = computed(() => {
|
||||||
if (query.value.trim()) return filteredItems.value.length
|
if (query.value.trim()) return filteredItems.value.length
|
||||||
return recentOffset.value + searchableItems.length
|
return recentOffset.value + allSearchableItems.value.length
|
||||||
})
|
})
|
||||||
|
|
||||||
const panelStyle = computed(() => {
|
const panelStyle = computed(() => {
|
||||||
@ -202,6 +221,20 @@ function getItemClass(index: number) {
|
|||||||
: 'hover:bg-white/10 text-white/90'
|
: '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) {
|
function selectItem(item: SearchableItem) {
|
||||||
spotlightStore.addRecentItem({
|
spotlightStore.addRecentItem({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@ -210,7 +243,9 @@ function selectItem(item: SearchableItem) {
|
|||||||
type: item.type,
|
type: item.type,
|
||||||
})
|
})
|
||||||
spotlightStore.close()
|
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()
|
cliStore.open()
|
||||||
} else if (item.path) {
|
} else if (item.path) {
|
||||||
router.push(item.path)
|
router.push(item.path)
|
||||||
@ -228,7 +263,9 @@ function selectHelpItem(section: { id: string }, item: { id: string; label: stri
|
|||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
spotlightStore.close()
|
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()
|
cliStore.open()
|
||||||
} else if (item.path) {
|
} else if (item.path) {
|
||||||
router.push(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' }) {
|
function selectRecent(item: { id: string; label: string; path?: string; type: 'navigate' | 'learn' | 'action' | 'goal' }) {
|
||||||
spotlightStore.close()
|
spotlightStore.close()
|
||||||
|
if (item.path?.startsWith('__launch_app__:')) {
|
||||||
|
launchInstalledApp(item.path.replace('__launch_app__:', ''))
|
||||||
|
return
|
||||||
|
}
|
||||||
if (item.path === '__cli__') {
|
if (item.path === '__cli__') {
|
||||||
cliStore.open()
|
cliStore.open()
|
||||||
return
|
return
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user