Skip to content

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

Merged
graycyrus merged 5 commits intotinyhumansai:mainfrom
graycyrus:feat/settings-skills-reorganize-396
Apr 9, 2026
Merged

fix(skills): per-card loading state on skill enable#454
graycyrus merged 5 commits intotinyhumansai:mainfrom
graycyrus:feat/settings-skills-reorganize-396

Conversation

@graycyrus
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus commented Apr 9, 2026

Summary

  • When clicking "Enable" on a third-party skill (Gmail, Notion, etc.), the entire skill list was replaced with a loading message ("Installing gmail..."), causing a jarring page flash
  • Now only the clicked card's button shows "Enabling..." with a disabled state while the install RPC runs
  • All other skill cards remain visible and interactive during install

Changes

  • app/src/pages/Skills.tsx: Removed installing from the full-list loading gate; pass isInstalling prop to individual ThirdPartySkillCard
  • app/src/components/skills/SkillCard.tsx: Added isInstalling prop to show "Enabling..." label and ctaDisabled prop to disable the CTA button with reduced opacity

Test plan

  • Click "Enable" on Gmail — only the Gmail card button shows "Enabling...", rest of page stays visible
  • Click "Enable" on Notion — same per-card behavior
  • Initial page load still shows "Loading skills..." while fetching skill list
  • After install completes, setup modal opens as before
  • All existing Skills page tests pass (yarn test --run src/pages/__tests__/Skills)

Summary by CodeRabbit

  • New Features

    • Added breadcrumb navigation to Settings for easier navigation across sections
    • Enhanced Skills page with search functionality and category filtering
    • Added unified skill card interface for consistent skill management
  • Refactoring

    • Reorganized Settings as a full route with improved structure and standardized spacing
    • Moved Browser Access toggle from Settings to the Skills page
    • Redesigned Skills page layout with better organization and control placement
    • Removed unused settings sections (Profile, Advanced)

…stency 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 tinyhumansai#396
- 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
- 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
…ll 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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

This PR refactors the Settings and Skills UI architecture. It removes deprecated settings panels and helper components, introduces breadcrumb navigation throughout the Settings UI via an updated useSettingsNavigation hook, decomposes large panel components into smaller reusable sub-components, and restructures the Skills page with new filtering, search, and browser access toggle functionality. The changes consolidate UI patterns and remove approximately 1,800 lines while adding 2,000+ lines of restructured components.

Changes

Cohort / File(s) Summary
Navigation & Routing
app/src/components/settings/hooks/useSettingsNavigation.ts, app/src/components/settings/components/SettingsSectionPage.tsx, app/src/components/settings/components/SettingsHeader.tsx
Extended SettingsHeader with optional breadcrumbs prop and breadcrumb rendering UI. Added BreadcrumbItem interface with label and optional onClick. Updated useSettingsNavigation hook to return computed breadcrumbs array derived from current route, and removed 'profile', 'advanced', and 'skills' route types.
Removed Settings Panels
app/src/components/settings/panels/SkillsPanel.tsx, app/src/components/settings/panels/TauriCommandsPanel.tsx, app/src/components/settings/panels/ProfilePanel.tsx, app/src/components/settings/panels/AdvancedPanel.tsx, app/src/pages/Settings.tsx
Deleted four complete settings panels: SkillsPanel (246 lines, skill toggle/enable management), TauriCommandsPanel (1,268 lines, CLI/diagnostics console), ProfilePanel and AdvancedPanel (placeholder "Coming soon" screens). Removed corresponding route entries and navigation menu items from Settings page.
Removed Helper Components
app/src/components/settings/components/SettingsPanelLayout.tsx, app/src/components/settings/components/SettingsBackButton.tsx, app/src/components/settings/hooks/useSettingsAnimation.ts, app/src/components/SkillsGrid.tsx
Deleted shared UI utilities no longer needed: SettingsPanelLayout container, SettingsBackButton standalone button (now integrated into breadcrumbs), useSettingsAnimation hook (animation state management), and SkillsGrid table component (217 lines with skill loading/display logic).
Settings Panels with Breadcrumbs
app/src/components/settings/panels/{AI,Accessibility,AgentChat,Billing,Connections,MemoryData,MemoryDebug,Messaging,Privacy,RecoveryPhrase,Team,TeamInvites,TeamManagement,TeamMembers,Tools,Voice,WebhooksDebug}Panel.tsx
17 settings panel files updated to consume breadcrumbs from useSettingsNavigation hook and pass as prop to SettingsHeader for breadcrumb navigation display. Minor layout adjustments: some panels changed spacing from space-y-3/space-y-5 to space-y-4, and ToolsPanel adjusted container padding.
Refactored Large Panels with Sub-components
app/src/components/settings/panels/LocalModelPanel.tsx, app/src/components/settings/panels/AutocompletePanel.tsx, app/src/components/settings/panels/CronJobsPanel.tsx, app/src/components/settings/panels/ScreenIntelligencePanel.tsx
Decomposed large monolithic panels into smaller focused components. LocalModelPanel (-770 lines) split into DeviceCapabilitySection, ModelStatusSection, ModelDownloadSection. AutocompletePanel (-270 lines) split into AppFilterSection, CompletionStyleSection. CronJobsPanel (-272 lines) split into CoreJobList, RuntimeSkillCronList. ScreenIntelligencePanel (-191 lines) split into PermissionsSection, SessionAndVisionSection. All panels updated to use breadcrumbs and import new sub-components.
New Settings Sub-components
app/src/components/settings/panels/autocomplete/{AppFilterSection,CompletionStyleSection}.tsx, app/src/components/settings/panels/cron/{CoreJobList,RuntimeSkillCronList}.tsx, app/src/components/settings/panels/local-model/{DeviceCapabilitySection,ModelStatusSection,ModelDownloadSection}.tsx, app/src/components/settings/panels/screen-intelligence/{PermissionsSection,SessionAndVisionSection}.tsx
Added 9 new presentational/functional sub-components extracted from refactored panels. Each manages a cohesive UI section (e.g., runtime status, model downloads, cron job controls, accessibility permissions, vision summaries) with props for state, loading/error flags, and callbacks.
Skills Page & New Components
app/src/pages/Skills.tsx, app/src/components/skills/SkillCard.tsx, app/src/components/skills/SkillSearchBar.tsx, app/src/components/skills/SkillCategoryFilter.tsx
Restructured Skills page: introduced BrowserAccessToggle component, added SkillSearchBar (search input with clear button), SkillCategoryFilter (category toggle buttons), and UnifiedSkillCard (single reusable skill display component replacing per-type variants). Added skill filtering/grouping by category and search query. Changed layout from separate skill section cards to unified filtered list with category sections. Updated tests for new overflow menu interaction pattern.
Skills Card Implementations
app/src/components/skills/SkillCard.tsx
New file exporting UnifiedSkillCard (base component with icon, title, status, description, sync UI, actions menu, CTA button) and ThirdPartySkillCard (wrapper connecting to skill status/sync hooks, managing debug modal, and deriving UI state from connection/sync status).
Documentation
.claude/memory.md
Added memory section documenting the settings/skills refactor: breadcrumb navigation system, removed/decomposed components, unified skill card rendering, and new filter/search UI for Skills page.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • senamakel

🐰 Breadcrumbs flow through settings fair,
Old panels fade into thin air,
Skills search and filter, unified in form,
Components decompose to make the code norm.
Navigation paths now clearly shining bright,
Settings UI refactored just right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the primary fix: implementing per-card loading state when enabling skills, addressing the bug where enabling a third-party skill would hide the entire skill list.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/settings-skills-reorganize-396

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (4)
app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx (1)

75-79: Scope the overflow-menu lookup to Gmail’s card.

screen.getByTitle('More actions') will become ambiguous as soon as another card renders the same control, so this test can start failing without any Gmail sync regression. Query the Gmail card first and use within(...) for the menu trigger and sync action.

As per coding guidelines, "prefer testing behavior over implementation details".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx` around lines
75 - 79, The test currently calls screen.getByTitle('More actions') which is
ambiguous; instead locate the Gmail card element first (e.g., query for its
heading or a unique container), then scope the overflow-menu lookup to that card
using within(gmailCard) to click the More actions button and to find the sync
action (test id 'skill-sync-button-gmail'); update the calls so you use
within(...) for both the menu trigger and the await find of
'skill-sync-button-gmail' to ensure the lookup is scoped to Gmail's card.
app/src/pages/Skills.tsx (1)

142-151: Consider logging runtime flag fetch failures in development.

The empty catch block silently swallows errors. While the comment explains the default behavior, logging in development mode could help diagnose connectivity or API issues.

♻️ Optional: Log errors in dev mode
   useEffect(() => {
     (async () => {
       try {
         const res = await openhumanGetRuntimeFlags();
         setBrowserAllowAll(res.result.browser_allow_all);
       } catch {
-        // Silently ignore — toggle defaults to false
+        // Toggle defaults to false on error
+        if (IS_DEV) console.debug('[BrowserAccessToggle] Failed to fetch runtime flags');
       }
     })();
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/Skills.tsx` around lines 142 - 151, The empty catch in the
useEffect around openhumanGetRuntimeFlags swallows errors; update the catch to
log the caught error in development only (e.g., check NODE_ENV or an isDev flag)
and include context (mention openhumanGetRuntimeFlags and setBrowserAllowAll) so
connectivity/API failures are visible during local testing while keeping
production behavior unchanged.
app/src/components/settings/panels/LocalModelPanel.tsx (1)

418-426: Consider memoizing callback props to prevent unnecessary re-renders.

The onApplyPreset callback is created inline on each render. For better performance, consider wrapping it with useCallback.

♻️ Optional: Memoize callback
+import { useCallback, useEffect, useMemo, useState } from 'react';
...
+const handleApplyPreset = useCallback((tier: string) => {
+  void applyPreset(tier);
+}, []);
...
         <DeviceCapabilitySection
           presetsData={presetsData}
           presetsLoading={presetsLoading}
           presetError={presetError}
           presetSuccess={presetSuccess}
           isApplyingPreset={isApplyingPreset}
-          onApplyPreset={tier => void applyPreset(tier)}
+          onApplyPreset={handleApplyPreset}
           formatRamGb={formatRamGb}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/LocalModelPanel.tsx` around lines 418 -
426, The inline arrow passed to DeviceCapabilitySection as the onApplyPreset
prop causes a new function each render; wrap the applyPreset call in a stable
callback using React's useCallback (e.g., create a memoized onApplyPreset that
calls applyPreset(tier)) and pass that memoized function to onApplyPreset to
avoid needless re-renders; reference the existing applyPreset function and the
onApplyPreset prop on DeviceCapabilitySection when implementing the useCallback.
app/src/components/settings/panels/autocomplete/AppFilterSection.tsx (1)

131-132: Prefer mutually exclusive feedback for success vs error

Lines 131–132 can render both message and error together. Consider giving error precedence to avoid contradictory status text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/autocomplete/AppFilterSection.tsx` around
lines 131 - 132, The component AppFilterSection currently renders both message
and error simultaneously which can show conflicting feedback; update the JSX to
make them mutually exclusive by giving error precedence—render error when the
error variable is truthy, otherwise render message—so only one status line
(error or success) is shown at a time (adjust the conditional rendering around
the message and error variables accordingly).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/settings/components/SettingsHeader.tsx`:
- Around line 45-77: Add the current page as the final, non-clickable breadcrumb
inside the existing <nav aria-label="Breadcrumb"> so screen readers receive a
complete trail: after rendering the mapped breadcrumbs (the breadcrumbs.map and
per-crumb <li> using crumb.label and crumb.onClick), append one more <li> that
renders the current page title (the same string used in the adjacent <h2>) as a
<span> with aria-current="page" and the same text styling, and ensure the
separator SVG is not rendered after this final item.

In `@app/src/components/settings/hooks/useSettingsNavigation.ts`:
- Around line 150-203: The hook is missing coverage for the "tools" settings
route so currentRoute falls back to 'home' and breadcrumbs/back-navigation
break; add "tools" to the SettingsRoute union/type, ensure getCurrentRoute()
maps the "/settings/tools" path to 'tools', and add a case 'tools' in
getBreadcrumbs (alongside 'ai-tools') returning [settingsCrumb, aiToolsCrumb] so
the breadcrumbs/back navigation behave correctly.

In `@app/src/components/settings/panels/autocomplete/AppFilterSection.tsx`:
- Around line 81-87: The textarea currently rendered with
value={contextOverride} and onChange={event =>
onSetContextOverride(event.target.value)} lacks an accessible label; add a
semantic <label> associated via htmlFor and give the textarea a matching id (for
example id="contextOverride") so assistive tech can identify it, updating the
surrounding <div> text to be a <label htmlFor="contextOverride">Context Override
(optional)</label> and ensure the textarea keeps its value and onChange handlers
(contextOverride and onSetContextOverride).
- Around line 43-45: In AppFilterSection.tsx the three lines rendering
status?.platform_supported, status?.enabled and status?.running currently
default to 'no' when status is null/undefined; update the rendering logic to
show 'unknown' for missing values by checking each property explicitly (e.g.,
for platform_supported, use status?.platform_supported === true ? 'yes' :
status?.platform_supported === false ? 'no' : 'unknown') and apply the same
pattern to enabled and running so users see 'unknown' until status is loaded.

In `@app/src/components/settings/panels/cron/CoreJobList.tsx`:
- Around line 75-76: The UI currently renders new
Date(job.next_run).toLocaleString() directly which can show "Invalid Date" for
malformed timestamps; update the rendering in CoreJobList.tsx (both the
occurrence in the list and the other occurrence around the second spot) to
validate the parsed date first (e.g., create a Date from job.next_run, check
isNaN(date.getTime()) or date.toString() !== 'Invalid Date') and then render
date.toLocaleString() when valid or a stable fallback string like "—" /
"Unknown" when invalid; make this change for the JSX expressions that currently
call new Date(job.next_run).toLocaleString().

In `@app/src/components/settings/panels/cron/RuntimeSkillCronList.tsx`:
- Around line 88-90: The draft fallback currently uses an empty string which can
hide server values; change how drafts are read so that when computing draft (the
variable derived from draftValues[optionKey] in RuntimeSkillCronList.tsx) you
fall back to option.value instead of ''. Update every place that computes draft
(the occurrences around optionKey, draftValues, draft, and busy using savingKey)
so the UI displays the server-provided option.value when no draft exists and
prevents saving accidental empty values.

In `@app/src/components/settings/panels/local-model/ModelStatusSection.tsx`:
- Around line 90-101: Clamp the progress value to [0,1] before using it in both
the inline style width and the percentage text; create a local clampedProgress
(e.g., const clampedProgress = Math.min(Math.max(progress ?? 0, 0), 1)) and
replace raw uses of progress in the width calculation (style={{ width:
`${Math.round((isIndeterminateDownload ? 1 : progress) * 100)}%` }}) and the
percentage rendering (`${Math.round(progress * 100)}%`) so they use
clampedProgress and still respect isIndeterminateDownload and isInstalling
branches.

In
`@app/src/components/settings/panels/screen-intelligence/SessionAndVisionSection.tsx`:
- Around line 114-124: The parent card div wrapping recentVisionSummaries (the
element using key={summary.id}) mistakenly includes the Tailwind class
text-stone-200 which conflicts with its darker child text colors; remove
text-stone-200 from that container's className (or replace it with a suitable
color like text-stone-800 or simply rely on child colors/inherit) so the card
does not unintentionally force a light text color that gets overridden or
inherited inconsistently.

In `@app/src/components/skills/SkillSearchBar.tsx`:
- Around line 28-49: The input and clear button in SkillSearchBar are missing
accessible names; update the input element (the text input using props
value/onChange/placeholder) to include an explicit label or an aria-label (e.g.,
aria-label={placeholder || "Search skills"}) and add an aria-label="Clear
search" to the clear button (the button with onClick={() => onChange('')} and
the SVG) so screen readers can identify both controls.

---

Nitpick comments:
In `@app/src/components/settings/panels/autocomplete/AppFilterSection.tsx`:
- Around line 131-132: The component AppFilterSection currently renders both
message and error simultaneously which can show conflicting feedback; update the
JSX to make them mutually exclusive by giving error precedence—render error when
the error variable is truthy, otherwise render message—so only one status line
(error or success) is shown at a time (adjust the conditional rendering around
the message and error variables accordingly).

In `@app/src/components/settings/panels/LocalModelPanel.tsx`:
- Around line 418-426: The inline arrow passed to DeviceCapabilitySection as the
onApplyPreset prop causes a new function each render; wrap the applyPreset call
in a stable callback using React's useCallback (e.g., create a memoized
onApplyPreset that calls applyPreset(tier)) and pass that memoized function to
onApplyPreset to avoid needless re-renders; reference the existing applyPreset
function and the onApplyPreset prop on DeviceCapabilitySection when implementing
the useCallback.

In `@app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx`:
- Around line 75-79: The test currently calls screen.getByTitle('More actions')
which is ambiguous; instead locate the Gmail card element first (e.g., query for
its heading or a unique container), then scope the overflow-menu lookup to that
card using within(gmailCard) to click the More actions button and to find the
sync action (test id 'skill-sync-button-gmail'); update the calls so you use
within(...) for both the menu trigger and the await find of
'skill-sync-button-gmail' to ensure the lookup is scoped to Gmail's card.

In `@app/src/pages/Skills.tsx`:
- Around line 142-151: The empty catch in the useEffect around
openhumanGetRuntimeFlags swallows errors; update the catch to log the caught
error in development only (e.g., check NODE_ENV or an isDev flag) and include
context (mention openhumanGetRuntimeFlags and setBrowserAllowAll) so
connectivity/API failures are visible during local testing while keeping
production behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 24d51c27-d6b4-43ed-b00b-a8f265adaf8c

📥 Commits

Reviewing files that changed from the base of the PR and between 886bbea and 3658b0b.

📒 Files selected for processing (50)
  • .claude/memory.md
  • app/src/components/SkillsGrid.tsx
  • app/src/components/settings/SettingsSectionPage.tsx
  • app/src/components/settings/components/SettingsBackButton.tsx
  • app/src/components/settings/components/SettingsHeader.tsx
  • app/src/components/settings/components/SettingsPanelLayout.tsx
  • app/src/components/settings/hooks/useSettingsAnimation.ts
  • app/src/components/settings/hooks/useSettingsNavigation.ts
  • app/src/components/settings/panels/AIPanel.tsx
  • app/src/components/settings/panels/AccessibilityPanel.tsx
  • app/src/components/settings/panels/AdvancedPanel.tsx
  • app/src/components/settings/panels/AgentChatPanel.tsx
  • app/src/components/settings/panels/AutocompletePanel.tsx
  • app/src/components/settings/panels/BillingPanel.tsx
  • app/src/components/settings/panels/ConnectionsPanel.tsx
  • app/src/components/settings/panels/CronJobsPanel.tsx
  • app/src/components/settings/panels/DeveloperOptionsPanel.tsx
  • app/src/components/settings/panels/LocalModelPanel.tsx
  • app/src/components/settings/panels/MemoryDataPanel.tsx
  • app/src/components/settings/panels/MemoryDebugPanel.tsx
  • app/src/components/settings/panels/MessagingPanel.tsx
  • app/src/components/settings/panels/PrivacyPanel.tsx
  • app/src/components/settings/panels/ProfilePanel.tsx
  • app/src/components/settings/panels/RecoveryPhrasePanel.tsx
  • app/src/components/settings/panels/ScreenIntelligencePanel.tsx
  • app/src/components/settings/panels/SkillsPanel.tsx
  • app/src/components/settings/panels/TauriCommandsPanel.tsx
  • app/src/components/settings/panels/TeamInvitesPanel.tsx
  • app/src/components/settings/panels/TeamManagementPanel.tsx
  • app/src/components/settings/panels/TeamMembersPanel.tsx
  • app/src/components/settings/panels/TeamPanel.tsx
  • app/src/components/settings/panels/ToolsPanel.tsx
  • app/src/components/settings/panels/VoicePanel.tsx
  • app/src/components/settings/panels/WebhooksDebugPanel.tsx
  • app/src/components/settings/panels/autocomplete/AppFilterSection.tsx
  • app/src/components/settings/panels/autocomplete/CompletionStyleSection.tsx
  • app/src/components/settings/panels/cron/CoreJobList.tsx
  • app/src/components/settings/panels/cron/RuntimeSkillCronList.tsx
  • app/src/components/settings/panels/local-model/DeviceCapabilitySection.tsx
  • app/src/components/settings/panels/local-model/ModelDownloadSection.tsx
  • app/src/components/settings/panels/local-model/ModelStatusSection.tsx
  • app/src/components/settings/panels/screen-intelligence/PermissionsSection.tsx
  • app/src/components/settings/panels/screen-intelligence/SessionAndVisionSection.tsx
  • app/src/components/skills/SkillCard.tsx
  • app/src/components/skills/SkillCategoryFilter.tsx
  • app/src/components/skills/SkillSearchBar.tsx
  • app/src/pages/Settings.tsx
  • app/src/pages/Skills.tsx
  • app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx
  • app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx
💤 Files with no reviewable changes (9)
  • app/src/components/settings/panels/ProfilePanel.tsx
  • app/src/components/settings/panels/AdvancedPanel.tsx
  • app/src/components/settings/components/SettingsPanelLayout.tsx
  • app/src/components/settings/panels/SkillsPanel.tsx
  • app/src/components/settings/components/SettingsBackButton.tsx
  • app/src/pages/Settings.tsx
  • app/src/components/settings/panels/TauriCommandsPanel.tsx
  • app/src/components/settings/hooks/useSettingsAnimation.ts
  • app/src/components/SkillsGrid.tsx

Comment on lines +45 to +77
{/* Breadcrumbs */}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav aria-label="Breadcrumb" className="mr-1">
<ol className="flex items-center gap-1">
{breadcrumbs.map((crumb, i) => (
<li key={i} className="flex items-center gap-1">
{crumb.onClick ? (
<button
onClick={crumb.onClick}
className="text-xs text-stone-400 hover:text-stone-600 transition-colors">
{crumb.label}
</button>
) : (
<span className="text-xs text-stone-400">{crumb.label}</span>
)}
<svg
aria-hidden="true"
className="w-3 h-3 text-stone-300"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</li>
))}
</ol>
</nav>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include the current page in the breadcrumb nav.

The <nav aria-label="Breadcrumb"> stops at the parent links and the active page name is only rendered in the adjacent <h2>, so screen readers never get a complete breadcrumb trail. Add the current page as the last non-clickable crumb with aria-current="page" instead of keeping it only outside the nav.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/components/SettingsHeader.tsx` around lines 45 -
77, Add the current page as the final, non-clickable breadcrumb inside the
existing <nav aria-label="Breadcrumb"> so screen readers receive a complete
trail: after rendering the mapped breadcrumbs (the breadcrumbs.map and per-crumb
<li> using crumb.label and crumb.onClick), append one more <li> that renders the
current page title (the same string used in the adjacent <h2>) as a <span> with
aria-current="page" and the same text styling, and ensure the separator SVG is
not rendered after this final item.

Comment on lines +150 to +203
const getBreadcrumbs = (): BreadcrumbItem[] => {
switch (currentRoute) {
// Section pages
case 'account':
case 'automation':
case 'ai-tools':
return [settingsCrumb];

// Leaf panels under account
case 'billing':
case 'recovery-phrase':
case 'team':
case 'connections':
return [settingsCrumb, accountCrumb];

// Leaf panels under automation
case 'accessibility':
case 'screen-intelligence':
case 'autocomplete':
case 'messaging':
case 'cron-jobs':
return [settingsCrumb, automationCrumb];

// Leaf panels under ai-tools
case 'voice':
case 'local-model':
case 'ai':
return [settingsCrumb, aiToolsCrumb];

// Team sub-pages
case 'team-members':
case 'team-invites':
return [settingsCrumb, accountCrumb, teamCrumb];

// Developer sub-pages
case 'webhooks-debug':
case 'memory-data':
case 'memory-debug':
return [settingsCrumb, developerCrumb];

// Other leaf pages
case 'privacy':
case 'agent-chat':
case 'developer-options':
return [settingsCrumb];

case 'home':
default:
return [];
}
};

const breadcrumbs = getBreadcrumbs();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that every nested /settings route is represented in useSettingsNavigation.
sed -n '1,230p' app/src/components/settings/hooks/useSettingsNavigation.ts
sed -n '252,320p' app/src/pages/Settings.tsx

Repository: tinyhumansai/openhuman

Length of output: 9657


Add tools to route coverage in the hook.

app/src/pages/Settings.tsx defines a /settings/tools route, but it's missing from the SettingsRoute type, getCurrentRoute(), and getBreadcrumbs(). When accessed, the route falls back to 'home', resulting in no breadcrumbs and incorrect back-navigation behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/hooks/useSettingsNavigation.ts` around lines 150
- 203, The hook is missing coverage for the "tools" settings route so
currentRoute falls back to 'home' and breadcrumbs/back-navigation break; add
"tools" to the SettingsRoute union/type, ensure getCurrentRoute() maps the
"/settings/tools" path to 'tools', and add a case 'tools' in getBreadcrumbs
(alongside 'ai-tools') returning [settingsCrumb, aiToolsCrumb] so the
breadcrumbs/back navigation behave correctly.

Comment on lines +43 to +45
<div>Platform supported: {status?.platform_supported ? 'yes' : 'no'}</div>
<div>Enabled: {status?.enabled ? 'yes' : 'no'}</div>
<div>Running: {status?.running ? 'yes' : 'no'}</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid showing "no" when status is unknown

Lines 43–45 currently render "no" when status is null/undefined, which can mislead users before status loads. Render "unknown" for missing values.

Suggested fix
+  const toYesNoUnknown = (value: boolean | undefined) =>
+    value === true ? 'yes' : value === false ? 'no' : 'unknown';
+
   return (
     <>
       <section className="rounded-2xl border border-stone-200 bg-white p-4 space-y-3">
         <h3 className="text-sm font-semibold text-stone-900">Runtime</h3>
         <div className="text-sm text-stone-700 space-y-1">
-          <div>Platform supported: {status?.platform_supported ? 'yes' : 'no'}</div>
-          <div>Enabled: {status?.enabled ? 'yes' : 'no'}</div>
-          <div>Running: {status?.running ? 'yes' : 'no'}</div>
+          <div>Platform supported: {toYesNoUnknown(status?.platform_supported)}</div>
+          <div>Enabled: {toYesNoUnknown(status?.enabled)}</div>
+          <div>Running: {toYesNoUnknown(status?.running)}</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/autocomplete/AppFilterSection.tsx` around
lines 43 - 45, In AppFilterSection.tsx the three lines rendering
status?.platform_supported, status?.enabled and status?.running currently
default to 'no' when status is null/undefined; update the rendering logic to
show 'unknown' for missing values by checking each property explicitly (e.g.,
for platform_supported, use status?.platform_supported === true ? 'yes' :
status?.platform_supported === false ? 'no' : 'unknown') and apply the same
pattern to enabled and running so users see 'unknown' until status is loaded.

Comment on lines +81 to +87
<div className="text-xs text-stone-600">Context Override (optional)</div>
<textarea
value={contextOverride}
onChange={event => onSetContextOverride(event.target.value)}
rows={3}
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Associate the textarea with a semantic <label>

On Line 81, the text is in a <div>, so the <textarea> on Line 82 has no explicit accessible name for assistive tech. Please use <label htmlFor> + id.

Suggested fix
-          <div className="text-xs text-stone-600">Context Override (optional)</div>
+          <label htmlFor="context-override" className="text-xs text-stone-600">
+            Context Override (optional)
+          </label>
           <textarea
+            id="context-override"
             value={contextOverride}
             onChange={event => onSetContextOverride(event.target.value)}
             rows={3}
             className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="text-xs text-stone-600">Context Override (optional)</div>
<textarea
value={contextOverride}
onChange={event => onSetContextOverride(event.target.value)}
rows={3}
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
<label htmlFor="context-override" className="text-xs text-stone-600">
Context Override (optional)
</label>
<textarea
id="context-override"
value={contextOverride}
onChange={event => onSetContextOverride(event.target.value)}
rows={3}
className="w-full rounded border border-stone-200 bg-stone-50 p-2 text-xs text-stone-700"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/autocomplete/AppFilterSection.tsx` around
lines 81 - 87, The textarea currently rendered with value={contextOverride} and
onChange={event => onSetContextOverride(event.target.value)} lacks an accessible
label; add a semantic <label> associated via htmlFor and give the textarea a
matching id (for example id="contextOverride") so assistive tech can identify
it, updating the surrounding <div> text to be a <label
htmlFor="contextOverride">Context Override (optional)</label> and ensure the
textarea keeps its value and onChange handlers (contextOverride and
onSetContextOverride).

Comment on lines +75 to +76
{new Date(job.next_run).toLocaleString()}
</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard timestamp formatting to avoid “Invalid Date” in UI.

If a run/job timestamp is malformed, this currently leaks Invalid Date to users. Add a small formatter fallback.

🛠️ Suggested patch
+const formatDateTime = (value: string): string => {
+  const date = new Date(value);
+  return Number.isNaN(date.getTime()) ? 'n/a' : date.toLocaleString();
+};
+
 const CoreJobList = ({
@@
-                    {new Date(job.next_run).toLocaleString()}
+                    {formatDateTime(job.next_run)}
@@
-                      {new Date(run.finished_at).toLocaleString()}
+                      {formatDateTime(run.finished_at)}

Also applies to: 129-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/cron/CoreJobList.tsx` around lines 75 -
76, The UI currently renders new Date(job.next_run).toLocaleString() directly
which can show "Invalid Date" for malformed timestamps; update the rendering in
CoreJobList.tsx (both the occurrence in the list and the other occurrence around
the second spot) to validate the parsed date first (e.g., create a Date from
job.next_run, check isNaN(date.getTime()) or date.toString() !== 'Invalid Date')
and then render date.toLocaleString() when valid or a stable fallback string
like "—" / "Unknown" when invalid; make this change for the JSX expressions that
currently call new Date(job.next_run).toLocaleString().

Comment on lines +88 to +90
const optionKey = `${skill.skillId}:${option.name}`;
const draft = draftValues[optionKey] ?? '';
const busy = savingKey === optionKey;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use option.value as fallback when draft entry is missing.

Current fallback to empty string can show blank controls and allow saving empty values even when a server-provided value exists.

🛠️ Suggested patch
                 {skill.options.map(option => {
                   const optionKey = `${skill.skillId}:${option.name}`;
-                  const draft = draftValues[optionKey] ?? '';
+                  const draft =
+                    draftValues[optionKey] ??
+                    (option.value == null ? '' : String(option.value));
                   const busy = savingKey === optionKey;

Also applies to: 121-127, 142-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/cron/RuntimeSkillCronList.tsx` around
lines 88 - 90, The draft fallback currently uses an empty string which can hide
server values; change how drafts are read so that when computing draft (the
variable derived from draftValues[optionKey] in RuntimeSkillCronList.tsx) you
fall back to option.value instead of ''. Update every place that computes draft
(the occurrences around optionKey, draftValues, draft, and busy using savingKey)
so the UI displays the server-provided option.value when no draft exists and
prevents saving accidental empty values.

Comment on lines +90 to +101
style={{ width: `${Math.round((isIndeterminateDownload ? 1 : progress) * 100)}%` }}
/>
</div>

<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500">
<span>
Progress:{' '}
{isInstalling
? 'Installing Ollama runtime...'
: isIndeterminateDownload
? 'Downloading (size unknown)'
: `${Math.round(progress * 100)}%`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp progress before rendering width/percentage.

A value outside [0, 1] can render invalid width/percentage output.

🛠️ Suggested patch
 const ModelStatusSection = ({
@@
 }: ModelStatusSectionProps) => {
+  const normalizedProgress = Math.min(1, Math.max(0, progress));
+
   return (
@@
-              style={{ width: `${Math.round((isIndeterminateDownload ? 1 : progress) * 100)}%` }}
+              style={{
+                width: `${Math.round((isIndeterminateDownload ? 1 : normalizedProgress) * 100)}%`,
+              }}
@@
-                  : `${Math.round(progress * 100)}%`}
+                  : `${Math.round(normalizedProgress * 100)}%`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/settings/panels/local-model/ModelStatusSection.tsx` around
lines 90 - 101, Clamp the progress value to [0,1] before using it in both the
inline style width and the percentage text; create a local clampedProgress
(e.g., const clampedProgress = Math.min(Math.max(progress ?? 0, 0), 1)) and
replace raw uses of progress in the width calculation (style={{ width:
`${Math.round((isIndeterminateDownload ? 1 : progress) * 100)}%` }}) and the
percentage rendering (`${Math.round(progress * 100)}%`) so they use
clampedProgress and still respect isIndeterminateDownload and isInstalling
branches.

Comment on lines +114 to +124
{recentVisionSummaries.map(summary => (
<div
key={summary.id}
className="rounded-xl border border-stone-200 bg-white p-3 text-xs text-stone-200">
<div className="text-stone-500">
{new Date(summary.captured_at_ms).toLocaleTimeString()} ·{' '}
{summary.app_name ?? 'Unknown App'}
{summary.window_title ? ` · ${summary.window_title}` : ''}
</div>
<div className="mt-1 text-stone-800">{summary.actionable_notes}</div>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Likely incorrect text color class on vision summary card.

Line 117 uses text-stone-200 for the summary card container, but the child elements use darker colors like text-stone-500 and text-stone-800. The text-stone-200 on the parent div appears to be a typo and may cause unexpected inheritance issues or is simply unused. Consider removing it or using an appropriate color if it was intended for something specific.

🐛 Suggested fix
             <div
               key={summary.id}
-              className="rounded-xl border border-stone-200 bg-white p-3 text-xs text-stone-200">
+              className="rounded-xl border border-stone-200 bg-white p-3 text-xs">
               <div className="text-stone-500">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/components/settings/panels/screen-intelligence/SessionAndVisionSection.tsx`
around lines 114 - 124, The parent card div wrapping recentVisionSummaries (the
element using key={summary.id}) mistakenly includes the Tailwind class
text-stone-200 which conflicts with its darker child text colors; remove
text-stone-200 from that container's className (or replace it with a suitable
color like text-stone-800 or simply rely on child colors/inherit) so the card
does not unintentionally force a light text color that gets overridden or
inherited inconsistently.

Comment on lines +28 to +49
<input
type="text"
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
className="w-full rounded-xl border border-stone-200 bg-white py-2 pl-9 pr-9 text-sm text-stone-900 placeholder-stone-400 focus:border-primary-300 focus:outline-none focus:ring-1 focus:ring-primary-200"
/>
{value && (
<button
type="button"
onClick={() => onChange('')}
className="absolute inset-y-0 right-3 flex items-center text-stone-400 hover:text-stone-600">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add accessible names to the search and clear controls.

The input currently relies on placeholder text, and the clear button is icon-only with no label. That makes the new filter hard to use with screen readers. Add an explicit label or aria-label for the input, and an aria-label such as "Clear search" for the button.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/skills/SkillSearchBar.tsx` around lines 28 - 49, The input
and clear button in SkillSearchBar are missing accessible names; update the
input element (the text input using props value/onChange/placeholder) to include
an explicit label or an aria-label (e.g., aria-label={placeholder || "Search
skills"}) and add an aria-label="Clear search" to the clear button (the button
with onClick={() => onChange('')} and the SVG) so screen readers can identify
both controls.

@graycyrus
Copy link
Copy Markdown
Contributor Author

Closing — will re-open on a dedicated branch off main.

@graycyrus graycyrus closed this Apr 9, 2026
@graycyrus graycyrus reopened this Apr 9, 2026
@graycyrus graycyrus merged commit 4f0513b into tinyhumansai:main Apr 9, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant