Files
AgentCompany/src/renderer/components/skills/SkillImportModal.tsx
T
ZacharyZhang-NY 75022f82c5 refactor(renderer): route all Phosphor icon imports through ui/icons barrel
Create src/renderer/components/ui/icons.ts as the single re-export point
for every Phosphor icon the renderer touches, then rewrite the 95 files
that previously imported directly from "@phosphor-icons/react" to pull
from the relative-path-to-barrel instead. The TitleBar commit earlier
in this series already used ui/icons; this commit extends the pattern
to the rest of the renderer.

Benefits now wired in:
  - one location where bundle tree-shaking can be reasoned about,
  - typos like Briefcase vs BriefCase show up against a finite list,
  - re-theming or swapping icon sets is a one-file edit,
  - a lint rule that flags raw @phosphor-icons/react imports can be
    added later without mass-rewriting call sites.

The barrel also re-exports the `Icon` type so call sites that type
icon props against the Phosphor component type work unchanged.

Test count stays at 363 passing; typecheck stays at 156 errors.
2026-04-16 23:37:35 -04:00

177 lines
5.3 KiB
TypeScript

import { useState, useCallback } from "react";
import {
FolderSimple,
GithubLogo,
Globe,
Link,
} from "../ui/icons";
import { Modal, ActionButton, Input } from "../ui";
import { useAppStore } from "../../state/store";
type TabId = "github" | "local" | "url" | "skills_sh";
const tabs: { id: TabId; label: string; icon: typeof GithubLogo }[] = [
{ id: "github", label: "GitHub", icon: GithubLogo },
{ id: "local", label: "Local Path", icon: FolderSimple },
{ id: "url", label: "HTTP URL", icon: Link },
{ id: "skills_sh", label: "skills.sh", icon: Globe },
];
function isValidGithubSource(value: string): boolean {
return /^(https:\/\/github\.com\/[\w.-]+\/[\w.-]+|[\w.-]+\/[\w.-]+)/.test(value.trim());
}
function isValidUrl(value: string): boolean {
try {
const url = new URL(value.trim());
return url.protocol === "https:" || url.protocol === "http:";
} catch {
return false;
}
}
export function SkillImportModal({
open,
onClose,
companyId,
onImported,
}: {
open: boolean;
onClose: () => void;
companyId: string;
onImported: () => void;
}) {
const addToast = useAppStore((s) => s.addToast);
const [activeTab, setActiveTab] = useState<TabId>("github");
const [source, setSource] = useState("");
const [isImporting, setIsImporting] = useState(false);
const resetForm = useCallback(() => {
setSource("");
setActiveTab("github");
setIsImporting(false);
}, []);
const handleClose = useCallback(() => {
resetForm();
onClose();
}, [onClose, resetForm]);
const handleBrowse = useCallback(async () => {
const result = await window.agentCompany.pickDirectory();
if (result.ok && result.data) {
setSource(result.data);
}
}, []);
const canImport = (() => {
const trimmed = source.trim();
if (!trimmed) return false;
switch (activeTab) {
case "github":
return isValidGithubSource(trimmed);
case "url":
return isValidUrl(trimmed);
case "local":
return trimmed.length > 0;
case "skills_sh":
return trimmed.length > 0;
default:
return false;
}
})();
const handleImport = useCallback(async () => {
const trimmed = source.trim();
if (!trimmed) return;
setIsImporting(true);
try {
const result = await window.agentCompany.importSkill(companyId, trimmed);
if (result.ok) {
const count = result.data.length;
addToast(`Imported ${count} skill${count !== 1 ? "s" : ""} successfully`);
onImported();
handleClose();
} else {
addToast(result.error.message, "danger");
}
} catch (error) {
addToast(error instanceof Error ? error.message : "Import failed", "danger");
} finally {
setIsImporting(false);
}
}, [source, companyId, addToast, onImported, handleClose]);
const placeholder: Record<TabId, string> = {
github: "https://github.com/owner/repo or owner/repo",
local: "Enter or browse local path",
url: "https://example.com/path/to/skill",
skills_sh: "skills.sh install command or URL",
};
return (
<Modal open={open} title="Import Skill" onClose={handleClose} width="560px">
<div className="space-y-5">
{/* Tab bar */}
<div className="flex gap-1 border-b border-border">
{tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
type="button"
className={`flex items-center gap-1.5 px-3 py-2 text-[12px] font-medium transition-colors ${
isActive
? "border-b-2 border-[color:var(--primary)] text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
onClick={() => {
setActiveTab(tab.id);
setSource("");
}}
>
<Icon size={14} />
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Tab content */}
<div className="space-y-3">
{activeTab === "local" ? (
<div className="flex gap-2">
<div className="flex-1">
<Input value={source} onChange={setSource} placeholder={placeholder.local} />
</div>
<ActionButton label="Browse" onClick={() => void handleBrowse()} />
</div>
) : (
<Input value={source} onChange={setSource} placeholder={placeholder[activeTab]} />
)}
{activeTab === "skills_sh" && (
<p className="text-[12px] text-muted-foreground">
Enter a skills.sh install command (e.g., <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">npx skills.sh install skill-name</code>) or a direct skills.sh URL.
</p>
)}
</div>
{/* Actions */}
<div className="flex justify-end gap-3 pt-2">
<ActionButton label="Cancel" onClick={handleClose} />
<ActionButton
label="Import"
tone="accent"
onClick={() => void handleImport()}
loading={isImporting}
disabled={!canImport}
/>
</div>
</div>
</Modal>
);
}