fix(skills): per-card loading state on skill enable (#454)

* 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.
This commit is contained in:
Cyrus Gray
2026-04-09 17:56:37 +05:30
committed by GitHub
parent 886bbea368
commit 4f0513b23a
2 changed files with 11 additions and 6 deletions
+8 -2
View File
@@ -39,6 +39,7 @@ export interface UnifiedSkillCardProps {
metricsText?: string;
};
syncSummaryText?: string;
ctaDisabled?: boolean;
}
const CTA_STYLES: Record<string, string> = {
@@ -60,6 +61,7 @@ export function UnifiedSkillCard({
secondaryActions,
syncProgress,
syncSummaryText,
ctaDisabled,
}: UnifiedSkillCardProps) {
const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
@@ -164,11 +166,12 @@ export function UnifiedSkillCard({
)}
<button
type="button"
disabled={ctaDisabled}
onClick={e => {
e.stopPropagation();
onCtaClick();
}}
className={`flex-shrink-0 rounded-lg border px-3 py-1.5 text-[11px] font-medium transition-colors ${ctaStyle}`}>
className={`flex-shrink-0 rounded-lg border px-3 py-1.5 text-[11px] font-medium transition-colors ${ctaStyle} ${ctaDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}>
{ctaLabel}
</button>
</div>
@@ -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<SkillHostConnectionState & Record<string, unknown>>(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={
+3 -4
View File
@@ -353,11 +353,9 @@ export default function Skills() {
onChange={setSelectedCategory}
/>
{skillsLoading || installing ? (
{skillsLoading ? (
<div className="py-8 text-center">
<p className="text-sm text-stone-400">
{installing ? `Installing ${installing}...` : 'Loading skills...'}
</p>
<p className="text-sm text-stone-400">Loading skills...</p>
</div>
) : filteredItems.length === 0 ? (
<div className="py-8 text-center">
@@ -406,6 +404,7 @@ export default function Skills() {
<ThirdPartySkillCard
key={item.id}
skill={item.skill!}
isInstalling={installing === item.id}
onSetup={() => openSkillSetup(item.skill!)}
/>
);