Skip to content

Commit a89b744

Browse files
committed
chore: edit sidebar icons
1 parent a454e28 commit a89b744

6 files changed

Lines changed: 153 additions & 27 deletions

File tree

desktop/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"typecheck": "vue-tsc --noEmit"
1818
},
1919
"dependencies": {
20+
"@hugeicons/core-free-icons": "^4.0.0",
21+
"@hugeicons/vue": "^1.0.5",
2022
"@iconify-json/hugeicons": "^1.2.24",
2123
"@iconify-json/simple-icons": "^1.2.63",
2224
"@iconify/utils": "^3.1.0",

desktop/pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/components/AppSidebar.vue

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<script setup lang="ts">
22
import { Icon } from '@iconify/vue'
3+
import { ToolCaseIcon } from '@hugeicons/core-free-icons'
4+
import { HugeiconsIcon } from '@hugeicons/vue'
35
import { storeToRefs } from 'pinia'
46
import { computed, ref } from 'vue'
57
import { useRoute } from 'vue-router'
@@ -8,14 +10,14 @@ const route = useRoute()
810
const uiStore = useUIStateStore()
911
const { state } = storeToRefs(uiStore)
1012
11-
type LeafNavItem = { icon: string; label: string; to: string }
13+
type LeafNavItem = { icon?: string; hugeIcon?: object; label: string; to: string }
1214
type GroupNavItem = { icon: string; label: string; children: LeafNavItem[] }
1315
1416
const isCollapsed = computed(() => state.value.leftSidebarCollapsed)
1517
1618
const navItems: Array<LeafNavItem | GroupNavItem> = [
1719
{ icon: 'hugeicons:dashboard-square-01', label: 'Dashboard', to: '/' },
18-
{ icon: 'hugeicons:tools', label: 'Tools', to: '/tools' },
20+
{ hugeIcon: ToolCaseIcon, label: 'Tools', to: '/tools' },
1921
{ icon: 'hugeicons:calendar-03', label: 'Cron Jobs', to: '/cron-jobs' },
2022
{ icon: 'hugeicons:puzzle', label: 'Integrations', to: '/integrations' },
2123
{ icon: 'hugeicons:archive', label: 'Memory', to: '/memory' },
@@ -111,7 +113,7 @@ const getNavItemKey = (item: LeafNavItem | GroupNavItem) => (isGroupNavItem(item
111113
: 'text-muted-foreground hover:text-foreground hover:bg-muted/25',
112114
]"
113115
>
114-
<Icon :icon="child.icon" class="size-4 shrink-0" />
116+
<Icon :icon="child.icon ?? 'hugeicons:circle'" class="size-4 shrink-0" />
115117
<span>{{ child.label }}</span>
116118
</RouterLink>
117119
</div>
@@ -132,7 +134,15 @@ const getNavItemKey = (item: LeafNavItem | GroupNavItem) => (isGroupNavItem(item
132134
]"
133135
:title="isCollapsed ? item.label : undefined"
134136
>
135-
<Icon :icon="item.icon" class="size-[18px] shrink-0" />
137+
<HugeiconsIcon
138+
v-if="item.hugeIcon"
139+
:icon="item.hugeIcon"
140+
:size="18"
141+
color="currentColor"
142+
:stroke-width="1.5"
143+
class="shrink-0"
144+
/>
145+
<Icon v-else :icon="item.icon ?? 'hugeicons:circle'" class="size-[18px] shrink-0" />
136146
<span v-if="!isCollapsed">{{ item.label }}</span>
137147
</RouterLink>
138148
</template>

desktop/src/pages/integrations.vue

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
1320
const auth = useAuthStore()
1421
const integrations = ref<Integration[]>([])
1522
const 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+
114134
function 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>

src/gateway/api.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,33 @@ pub async fn handle_api_integrations(
515515
Json(serde_json::json!({"integrations": integrations})).into_response()
516516
}
517517

518+
/// GET /api/skills — list installed skills for desktop catalog views
519+
pub async fn handle_api_skills(
520+
State(state): State<AppState>,
521+
headers: HeaderMap,
522+
) -> impl IntoResponse {
523+
if let Err(e) = require_auth(&state, &headers) {
524+
return e.into_response();
525+
}
526+
527+
let config = state.config.lock().clone();
528+
let skills = crate::skills::load_skills_with_config(&config.workspace_dir, &config);
529+
530+
let entries: Vec<serde_json::Value> = skills
531+
.iter()
532+
.map(|skill| {
533+
serde_json::json!({
534+
"name": skill.name,
535+
"description": skill.description,
536+
"category": "Skills",
537+
"status": "Active",
538+
})
539+
})
540+
.collect();
541+
542+
Json(serde_json::json!({"skills": entries})).into_response()
543+
}
544+
518545
/// POST /api/doctor — run diagnostics
519546
pub async fn handle_api_doctor(
520547
State(state): State<AppState>,

src/gateway/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ pub async fn run_gateway(host: &str, port: u16, config: Config) -> Result<()> {
679679
.route("/api/cron", post(api::handle_api_cron_add))
680680
.route("/api/cron/{id}", delete(api::handle_api_cron_delete))
681681
.route("/api/integrations", get(api::handle_api_integrations))
682+
.route("/api/skills", get(api::handle_api_skills))
682683
.route(
683684
"/api/doctor",
684685
get(api::handle_api_doctor).post(api::handle_api_doctor),

0 commit comments

Comments
 (0)