From 4f0513b23ad65f0f6e4ac858fa82c96faaaecb60 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:56:37 +0530 Subject: [PATCH] fix(skills): per-card loading state on skill enable (#454) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(settings,skills): reorganize settings and skills pages for consistency and usability - Remove dead code: TauriCommandsPanel, useSettingsAnimation, SettingsPanelLayout, SettingsBackButton, ProfilePanel, AdvancedPanel, SkillsPanel, SkillsGrid (~1900 lines) - Standardize settings panel padding to p-4 space-y-4 across all panels - Add breadcrumb navigation to SettingsHeader with route-derived breadcrumbs - Decompose oversized panels: LocalModelPanel, AutocompletePanel, CronJobsPanel, ScreenIntelligencePanel into sub-components in dedicated subdirectories - Deduplicate skills management: move browser access toggle to Skills page, remove redundant /settings/skills route - Unify skill card layout: UnifiedSkillCard component with overflow menu for secondary actions, consistent status/CTA patterns across all skill types - Add skill search bar and category filter with grouped results Closes #396 * fix(settings,skills): address CodeRabbit review feedback - Use semantic nav/ol/li for breadcrumbs with aria-hidden separators - Fix duplicate text-xs class in CompletionStyleSection - Use interface instead of type for CronSkillConfig - Disable cron option inputs when parent skill is disabled - Deduplicate preset error rendering in DeviceCapabilitySection - Fix low-contrast labels: text-stone-300 → text-stone-700, text-stone-200 → text-stone-600 - Disable "Set Path" button when input is empty - Add aria-pressed to category filter buttons - Convert SkillCategoryFilter to arrow function - Use void operator for async onClick handler - Simplify redundant conditional in ThirdPartySkillCard - Use async/await instead of .then() in BrowserAccessToggle * fix(tests): update Skills page tests for unified card layout - Mock openhumanGetRuntimeFlags/openhumanSetBrowserAllowAll for BrowserAccessToggle - Open overflow menu before clicking sync/debug buttons (now in secondary actions) - Remove assertion for deleted "3rd Party Skills" heading * fix(skills): show per-card loading state instead of hiding entire skill list on enable When clicking Enable on a third-party skill (e.g. Gmail, Notion), the entire skill list was replaced with a loading message. Now only the clicked card's button shows "Enabling..." with a disabled state while the install RPC runs, keeping all other cards visible and interactive. --- app/src/components/skills/SkillCard.tsx | 10 ++++++++-- app/src/pages/Skills.tsx | 7 +++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/components/skills/SkillCard.tsx b/app/src/components/skills/SkillCard.tsx index a80076592..c5ebb23f7 100644 --- a/app/src/components/skills/SkillCard.tsx +++ b/app/src/components/skills/SkillCard.tsx @@ -39,6 +39,7 @@ export interface UnifiedSkillCardProps { metricsText?: string; }; syncSummaryText?: string; + ctaDisabled?: boolean; } const CTA_STYLES: Record = { @@ -60,6 +61,7 @@ export function UnifiedSkillCard({ secondaryActions, syncProgress, syncSummaryText, + ctaDisabled, }: UnifiedSkillCardProps) { const [menuOpen, setMenuOpen] = useState(false); const menuRef = useRef(null); @@ -164,11 +166,12 @@ export function UnifiedSkillCard({ )} @@ -180,9 +183,10 @@ export function UnifiedSkillCard({ interface ThirdPartySkillCardProps { skill: SkillListEntry; onSetup: () => void; + isInstalling?: boolean; } -export function ThirdPartySkillCard({ skill, onSetup }: ThirdPartySkillCardProps) { +export function ThirdPartySkillCard({ skill, onSetup, isInstalling }: ThirdPartySkillCardProps) { const connectionStatus = useSkillConnectionStatus(skill.id); const statusDisplay = STATUS_DISPLAY[connectionStatus] ?? STATUS_DISPLAY.offline; const skillState = useSkillState>(skill.id); @@ -239,6 +243,7 @@ export function ThirdPartySkillCard({ skill, onSetup }: ThirdPartySkillCardProps } function ctaLabel(): string { + if (isInstalling) return 'Enabling...'; switch (connectionStatus) { case 'offline': return 'Enable'; case 'setup_required': return 'Setup'; @@ -305,6 +310,7 @@ export function ThirdPartySkillCard({ skill, onSetup }: ThirdPartySkillCardProps statusColor={statusDisplay.color} ctaLabel={ctaLabel()} ctaVariant={ctaVariant()} + ctaDisabled={isInstalling} onCtaClick={() => onSetup()} secondaryActions={secondaryActions} syncProgress={ diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx index 59bfd139b..8fe205491 100644 --- a/app/src/pages/Skills.tsx +++ b/app/src/pages/Skills.tsx @@ -353,11 +353,9 @@ export default function Skills() { onChange={setSelectedCategory} /> - {skillsLoading || installing ? ( + {skillsLoading ? (
-

- {installing ? `Installing ${installing}...` : 'Loading skills...'} -

+

Loading skills...

) : filteredItems.length === 0 ? (
@@ -406,6 +404,7 @@ export default function Skills() { openSkillSetup(item.skill!)} /> );