diff --git a/.claude/plans/reflective-meandering-castle.md b/.claude/plans/reflective-meandering-castle.md index 3f6676aa..4c3e5700 100644 --- a/.claude/plans/reflective-meandering-castle.md +++ b/.claude/plans/reflective-meandering-castle.md @@ -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 diff --git a/neode-ui/src/components/SpotlightSearch.vue b/neode-ui/src/components/SpotlightSearch.vue index 8d7e7e46..9d35b39d 100644 --- a/neode-ui/src/components/SpotlightSearch.vue +++ b/neode-ui/src/components/SpotlightSearch.vue @@ -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(null) const panelRef = ref(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(() => { + 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