@@ -10,6 +10,13 @@ interface Integration {
1010 status: ' Available' | ' Active' | ' ComingSoon'
1111}
1212
13+ interface SkillCatalogEntry {
14+ name: string
15+ description: string
16+ category? : string
17+ status? : ' Available' | ' Active' | ' ComingSoon'
18+ }
19+
1320const auth = useAuthStore ()
1421const integrations = ref <Integration []>([])
1522const loading = ref (true )
@@ -101,7 +108,7 @@ function normalizeEntries(entries: Integration[]): Integration[] {
101108 }))
102109}
103110
104- function normalizeResponse (response : Integration [] | { integrations? : Integration [] }): Integration [] {
111+ function normalizeIntegrationResponse (response : Integration [] | { integrations? : Integration [] }): Integration [] {
105112 if (Array .isArray (response )) {
106113 return normalizeEntries (response )
107114 }
@@ -111,6 +118,19 @@ function normalizeResponse(response: Integration[] | { integrations?: Integratio
111118 return []
112119}
113120
121+ function normalizeSkillsResponse(response : SkillCatalogEntry [] | { skills? : SkillCatalogEntry [] }): Integration [] {
122+ const entries = Array .isArray (response )
123+ ? response
124+ : (response && Array .isArray (response .skills ) ? response .skills : [])
125+
126+ return normalizeEntries (entries .map (skill => ({
127+ name: skill .name ,
128+ description: skill .description ,
129+ category: skill .category ?? ' Skills' ,
130+ status: skill .status ?? ' Active' ,
131+ })))
132+ }
133+
114134function statusIcon(status : Integration [' status' ]): string {
115135 if (status === ' Active' ) {
116136 return ' hugeicons:tick-02'
@@ -191,19 +211,50 @@ const integrationLogoMap: Record<string, IntegrationLogo> = {
191211 Android: { icon: ' simple-icons:android' , color: ' #3DDC84' },
192212}
193213
194- function integrationIcon(name : string ): string {
195- return integrationLogoMap [name ]?.icon ?? ' hugeicons:puzzle'
214+ const skillIconMatchers: Array <{ pattern: RegExp , logo: IntegrationLogo }> = [
215+ { pattern: / docx| word| document/ , logo: { icon: ' simple-icons:microsoftword' , color: ' #2B579A' } },
216+ { pattern: / xlsx| excel| sheet| spreadsheet/ , logo: { icon: ' simple-icons:microsoftexcel' , color: ' #217346' } },
217+ { pattern: / pptx| powerpoint| slides? | presentation/ , logo: { icon: ' simple-icons:microsoftpowerpoint' , color: ' #B7472A' } },
218+ { pattern: / pdf/ , logo: { icon: ' simple-icons:adobeacrobatreader' , color: ' #EC1C24' } },
219+ { pattern: / github/ , logo: { icon: ' simple-icons:github' } },
220+ { pattern: / slack/ , logo: { icon: ' simple-icons:slack' , color: ' #4A154B' } },
221+ { pattern: / notion/ , logo: { icon: ' simple-icons:notion' } },
222+ { pattern: / vercel/ , logo: { icon: ' simple-icons:vercel' } },
223+ ]
224+
225+ function resolveIcon(name : string , category : string ): IntegrationLogo | undefined {
226+ if (category === ' Skills' ) {
227+ for (const matcher of skillIconMatchers ) {
228+ if (matcher .pattern .test (name .toLowerCase ())) {
229+ return matcher .logo
230+ }
231+ }
232+ }
233+
234+ return integrationLogoMap [name ]
235+ }
236+
237+ function integrationIcon(name : string , category : string ): string {
238+ const logo = resolveIcon (name , category )
239+ if (logo ) {
240+ return logo .icon
241+ }
242+ if (category === ' Skills' ) {
243+ return ' hugeicons:airplay-line'
244+ }
245+ return ' hugeicons:puzzle'
196246}
197247
198- function integrationIconClass(name : string ): string {
199- if (! integrationLogoMap [name ]) {
248+ function integrationIconClass(name : string , category : string ): string {
249+ const logo = resolveIcon (name , category )
250+ if (! logo ) {
200251 return ' text-primary'
201252 }
202- return integrationLogoMap [ name ]? .color ? ' ' : ' text-foreground'
253+ return logo .color ? ' ' : ' text-foreground'
203254}
204255
205- function integrationIconStyle(name : string ): { color: string } | undefined {
206- const color = integrationLogoMap [ name ] ?.color
256+ function integrationIconStyle(name : string , category : string ): { color: string } | undefined {
257+ const color = resolveIcon ( name , category ) ?.color
207258 return color ? { color } : undefined
208259}
209260
@@ -214,18 +265,33 @@ async function fetchIntegrations(showLoading = true) {
214265
215266 error .value = null
216267
217- try {
218- const response = await auth .fetchWithAuth <Integration [] | { integrations? : Integration [] }>(' /api/integrations' )
219- integrations .value = normalizeResponse (response )
268+ const [integrationsResult, skillsResult] = await Promise .allSettled ([
269+ auth .fetchWithAuth <Integration [] | { integrations? : Integration [] }>(' /api/integrations' ),
270+ auth .fetchWithAuth <SkillCatalogEntry [] | { skills? : SkillCatalogEntry [] }>(' /api/skills' ),
271+ ])
272+
273+ const nextEntries: Integration [] = []
274+ const nextErrors: string [] = []
275+
276+ if (integrationsResult .status === ' fulfilled' ) {
277+ nextEntries .push (... normalizeIntegrationResponse (integrationsResult .value ))
278+ } else {
279+ nextErrors .push (' Failed to load integrations' )
220280 }
221- catch (err : unknown ) {
222- error .value = err instanceof Error ? err .message : ' Failed to load integrations'
223- integrations .value = []
281+
282+ if (skillsResult .status === ' fulfilled' ) {
283+ nextEntries .push (... normalizeSkillsResponse (skillsResult .value ))
284+ } else {
285+ // Backward compatibility: older daemons do not expose /api/skills yet.
286+ // Keep integrations usable and avoid surfacing a hard error for this optional section.
287+ console .warn (' Skills catalog unavailable:' , skillsResult .reason )
224288 }
225- finally {
226- if (showLoading ) {
227- loading .value = false
228- }
289+
290+ integrations .value = nextEntries
291+ error .value = nextErrors .length > 0 ? nextErrors .join (' · ' ) : null
292+
293+ if (showLoading ) {
294+ loading .value = false
229295 }
230296}
231297
@@ -243,7 +309,7 @@ onMounted(() => {
243309 <input
244310 v-model =" searchQuery"
245311 type =" text"
246- placeholder =" Search integrations..."
312+ placeholder =" Search integrations and skills ..."
247313 class =" w-full pl-10 pr-4 py-2 text-[13px] bg-card/50 border border-border/50 rounded-xl focus:outline-none focus:ring-1 focus:ring-primary/50 focus:border-primary/50 text-foreground placeholder:text-muted-foreground"
248314 >
249315 </div >
@@ -287,7 +353,7 @@ onMounted(() => {
287353 </div >
288354
289355 <div v-if =" Object.keys(groupedIntegrations).length === 0" class =" flex items-center justify-center h-full" >
290- <p class =" text-muted-foreground text-[12px]" >No integrations found.</p >
356+ <p class =" text-muted-foreground text-[12px]" >No integrations or skills found.</p >
291357 </div >
292358
293359 <div v-else class =" space-y-6" >
@@ -301,10 +367,10 @@ onMounted(() => {
301367 >
302368 <div class =" flex items-center gap-2 pr-2 mb-1" >
303369 <Icon
304- :icon =" integrationIcon (integration .name )"
370+ :icon =" integrationIcon (integration .name , integration . category )"
305371 class="size-[16px] shrink-0"
306- :class =" integrationIconClass (integration .name )"
307- :style =" integrationIconStyle (integration .name )"
372+ :class =" integrationIconClass (integration .name , integration . category )"
373+ :style =" integrationIconStyle (integration .name , integration . category )"
308374 />
309375 <h3 class =" font-sans text-[15px] font-medium text-foreground truncate" >{{ integration.name }}</h3 >
310376 </div >
0 commit comments