Handling the status of skills (#23)

* chore: bump version to 0.20.0 [skip ci]

* revert: remove automatic skill dependency installation system

- Removed dependency installation loading state from SkillSetupWizard
- Reverted SkillRuntime dependency checking and installation methods
- Cleaned up dependency-related imports and event listeners
- Documented complete implementation in skills/todo.md for future work

The dependency installation system was causing unwanted loading states
when configuring skills. All functionality has been properly reverted
while preserving implementation details for future development.

* Refactor SkillManagementPanel to conditionally show action buttons based on connection status

- Added offline and setup-required states with informative messages
- Updated SkillSetupModal to use connection status for setup vs. manage mode
- Simplified mode logic for better UX and error handling in setup scenarios

* Update CLAUDE.md with new features, state slices, and code quality tools

- Document additions: Redux aiSlice, skillsSlice, teamSlice, and architecture updates
- Add ESLint, Prettier integrations with Husky hooks and GitHub workflows
- Update development commands from npm to yarn for consistency
- Expand key user groups and revise target audiences
- Include enhanced project structure, build outputs, and CI/CD updates

* fix: resolve TypeScript compilation error and enhance skill session synchronization

- Fix unused parameter error in SkillManager.getSkillLoadParams() by prefixing with underscore
- Add support for session parameter passing in SkillRuntime.load() method
- Implement skill reloading capability for post-authentication updates
- Ensure proper connection status synchronization between skill setup and UI display

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: change skill button text from 'Configure' to 'Manage' for connected skills

- Update SkillsGrid button text to dynamically show based on connection status
- Connected skills now show 'Manage' button instead of 'Configure'
- Provides clearer user indication of skill state
- Improves UX for authenticated skills like Telegram

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: update documentation, download system and code formatting improvements

- Update Claude rules with improved project overview and command documentation
- Enhance download screen with better architecture detection and formatting
- Improve device detection utilities with multi-architecture support
- Add skills system troubleshooting documentation
- Code formatting improvements with better line breaks and spacing
- Optimize GitHub release asset parsing for platform-specific downloads

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Cyrus Gray
2026-02-03 05:35:54 +05:30
committed by GitHub
co-authored by Claude Steven Enamakel github-actions[bot] <github-actions[bot]@users.noreply.github.com>
parent 9d7472131f
commit 9b3dfd0c2d
14 changed files with 549 additions and 160 deletions
+16 -10
View File
@@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import {
type Architecture,
type ArchitectureDownloadLink,
detectPlatform,
fetchLatestRelease,
getDownloadLink,
getPlatformDisplayName,
parseReleaseAssets,
parseReleaseAssetsByArchitecture,
type Architecture,
type ArchitectureDownloadLink,
type Platform,
type PlatformArchitectureLinks,
type PlatformDownloadLinks,
@@ -33,7 +33,9 @@ const DownloadScreen = () => {
const [selectedPlatform, setSelectedPlatform] = useState<Platform | null>(null);
const [selectedArchitecture, setSelectedArchitecture] = useState<Architecture | null>(null);
const [releaseLinks, setReleaseLinks] = useState<PlatformDownloadLinks | null>(null);
const [architectureLinks, setArchitectureLinks] = useState<PlatformArchitectureLinks | null>(null);
const [architectureLinks, setArchitectureLinks] = useState<PlatformArchitectureLinks | null>(
null
);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -63,7 +65,9 @@ const DownloadScreen = () => {
const platformArchLinks = archLinks[detected.platform as keyof PlatformArchitectureLinks];
if (platformArchLinks && platformArchLinks.length > 0) {
// Prefer detected architecture, otherwise use first available
const preferredLink = platformArchLinks.find(link => link.architecture === detected.architecture) || platformArchLinks[0];
const preferredLink =
platformArchLinks.find(link => link.architecture === detected.architecture) ||
platformArchLinks[0];
setSelectedArchitecture(preferredLink.architecture);
}
} catch (err) {
@@ -88,7 +92,8 @@ const DownloadScreen = () => {
return getDownloadLink(selectedPlatform || 'unknown', releaseLinks || undefined);
}
const platformArchLinks = architectureLinks[selectedPlatform as keyof PlatformArchitectureLinks];
const platformArchLinks =
architectureLinks[selectedPlatform as keyof PlatformArchitectureLinks];
if (platformArchLinks && selectedArchitecture) {
const link = platformArchLinks.find(l => l.architecture === selectedArchitecture);
if (link) {
@@ -139,9 +144,7 @@ const DownloadScreen = () => {
{error && !isLoading && (
<div className="mb-6">
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-4">
<p className="text-sm text-red-400">
{error}. Using fallback download links.
</p>
<p className="text-sm text-red-400">{error}. Using fallback download links.</p>
</div>
</div>
)}
@@ -209,9 +212,12 @@ const DownloadScreen = () => {
{downloadOptions
.filter(opt => opt.platform !== selectedPlatform)
.map(option => {
const platformArchLinks = architectureLinks?.[option.platform as keyof PlatformArchitectureLinks];
const platformArchLinks =
architectureLinks?.[option.platform as keyof PlatformArchitectureLinks];
const hasValidLink = platformArchLinks && platformArchLinks.length > 0;
const defaultLink = platformArchLinks?.[0]?.url || getDownloadLink(option.platform, releaseLinks || undefined);
const defaultLink =
platformArchLinks?.[0]?.url ||
getDownloadLink(option.platform, releaseLinks || undefined);
const hasMultipleArchs = platformArchLinks && platformArchLinks.length > 1;
return (
+1 -1
View File
@@ -408,7 +408,7 @@ export default function SkillsGrid() {
setSetupModalOpen(true);
}}
className="px-4 py-1.5 text-xs font-medium text-primary-300 bg-primary-500/10 border border-primary-500/30 rounded-lg hover:bg-primary-500/20 transition-colors flex-shrink-0 ml-3">
Configure
{connectionStatus === 'connected' ? 'Manage' : 'Configure'}
</button>
</div>
);
+49 -40
View File
@@ -145,50 +145,59 @@ export default function SkillManagementPanel({
</div>
)}
{/* Action buttons — single row */}
<div className="pt-1">
{!confirmDisconnect ? (
<div className="flex space-x-2">
<button
onClick={handleRestart}
disabled={restarting}
className="flex-1 px-3 py-2 text-xs font-medium text-white bg-stone-700/60 border border-stone-600/50 rounded-xl hover:bg-stone-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{restarting ? "Restarting..." : "Restart"}
</button>
{onReconfigure && (
{/* Action buttons — only show if skill is actually running */}
{connectionStatus !== "offline" && connectionStatus !== "setup_required" ? (
<div className="pt-1">
{!confirmDisconnect ? (
<div className="flex space-x-2">
<button
onClick={onReconfigure}
className="flex-1 px-3 py-2 text-xs font-medium text-primary-300 bg-primary-500/10 border border-primary-500/30 rounded-xl hover:bg-primary-500/20 transition-colors"
onClick={handleRestart}
disabled={restarting}
className="flex-1 px-3 py-2 text-xs font-medium text-white bg-stone-700/60 border border-stone-600/50 rounded-xl hover:bg-stone-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
Re-run Setup
{restarting ? "Restarting..." : "Restart"}
</button>
)}
<button
onClick={() => setConfirmDisconnect(true)}
className="flex-1 px-3 py-2 text-xs font-medium text-coral-400 bg-coral-500/10 border border-coral-500/30 rounded-xl hover:bg-coral-500/20 transition-colors"
>
Disconnect
</button>
{onReconfigure && (
<button
onClick={onReconfigure}
className="flex-1 px-3 py-2 text-xs font-medium text-primary-300 bg-primary-500/10 border border-primary-500/30 rounded-xl hover:bg-primary-500/20 transition-colors"
>
Re-run Setup
</button>
)}
<button
onClick={() => setConfirmDisconnect(true)}
className="flex-1 px-3 py-2 text-xs font-medium text-coral-400 bg-coral-500/10 border border-coral-500/30 rounded-xl hover:bg-coral-500/20 transition-colors"
>
Disconnect
</button>
</div>
) : (
<div className="flex space-x-2">
<button
onClick={() => setConfirmDisconnect(false)}
className="flex-1 px-3 py-2 text-xs font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors"
>
Cancel
</button>
<button
onClick={handleDisconnect}
disabled={disconnecting}
className="flex-1 px-3 py-2 text-xs font-medium text-white bg-coral-500 rounded-xl hover:bg-coral-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{disconnecting ? "Disconnecting..." : "Confirm Disconnect"}
</button>
</div>
)}
</div>
) : (
// Show message for offline skills
<div className="pt-1">
<div className="text-xs text-stone-500 text-center py-2 px-3 bg-stone-800/30 rounded-xl border border-stone-700/30">
Skill is offline. Use "Re-run Setup" to reconnect.
</div>
) : (
<div className="flex space-x-2">
<button
onClick={() => setConfirmDisconnect(false)}
className="flex-1 px-3 py-2 text-xs font-medium text-stone-400 bg-stone-800/50 border border-stone-700 rounded-xl hover:bg-stone-800 transition-colors"
>
Cancel
</button>
<button
onClick={handleDisconnect}
disabled={disconnecting}
className="flex-1 px-3 py-2 text-xs font-medium text-white bg-coral-500 rounded-xl hover:bg-coral-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{disconnecting ? "Disconnecting..." : "Confirm Disconnect"}
</button>
</div>
)}
</div>
</div>
)}
</div>
);
}
+16 -8
View File
@@ -7,6 +7,7 @@
import { useState, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { useAppSelector } from "../../store/hooks";
import { useSkillConnectionStatus } from "../../lib/skills/hooks";
import SkillSetupWizard from "./SkillSetupWizard";
import SkillManagementPanel from "./SkillManagementPanel";
@@ -27,13 +28,20 @@ export default function SkillSetupModal({
onClose,
}: SkillSetupModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const setupComplete = useAppSelector(
(state) => state.skills.skills[skillId]?.setupComplete,
);
// Skills without setup hooks always go straight to manage mode.
const [mode, setMode] = useState<"manage" | "setup">(
!hasSetup || setupComplete ? "manage" : "setup",
);
const skill = useAppSelector((state) => state.skills.skills[skillId]);
const connectionStatus = useSkillConnectionStatus(skillId);
// Simplified mode logic: Only show manage mode when skill is actually working
// Everything else (offline, errors, never connected, etc.) goes to setup mode
const [mode, setMode] = useState<"manage" | "setup">(() => {
// Only show manage mode when the skill is actually working
const isWorking = connectionStatus === "connected" || connectionStatus === "connecting";
// For skills without setup hooks, still need to check if they're working
// Show manage mode only if skill is currently working
// Everything else goes to setup mode (first-time setup, errors, offline, disconnected)
return isWorking ? "manage" : "setup";
});
// Handle escape key
useEffect(() => {
@@ -137,7 +145,7 @@ export default function SkillSetupModal({
<SkillSetupWizard
skillId={skillId}
onComplete={onClose}
onCancel={setupComplete ? () => setMode("manage") : onClose}
onCancel={skill?.setupComplete ? () => setMode("manage") : onClose}
/>
)}
</div>
@@ -30,6 +30,7 @@ export default function SkillSetupWizard({
}: SkillSetupWizardProps) {
const [state, setState] = useState<WizardState>({ phase: "loading" });
// Start the skill (if not running) then start the setup flow on mount
useEffect(() => {
let cancelled = false;
@@ -162,6 +163,7 @@ export default function SkillSetupWizard({
</div>
);
case "step":
return (
<SetupFormRenderer