update
This commit is contained in:
1 parent
806200b3d1
commit
5547c29491
216 files changed
+5043
-3352
No files matched your search
@@ -10,9 +10,10 @@ import {
|
||||
} from "../ui/icons";
|
||||
import type { AgentRecord, SkillRecord, SkillUpdateStatus } from "@shared/types";
|
||||
import { useAppStore } from "../../state/store";
|
||||
import { ActionButton, StatusPill } from "../ui";
|
||||
import { ActionButton, StatusPill, Eyebrow } from "../ui";
|
||||
import { SkillAgentToggles } from "./SkillAgentToggles";
|
||||
import { buildTree, SkillFileTree } from "./SkillFileTree";
|
||||
import { useT, type TranslationFn } from "../../i18n";
|
||||
|
||||
const sourceIconMap: Record<string, typeof GithubLogo> = {
|
||||
github: GithubLogo,
|
||||
@@ -22,13 +23,22 @@ const sourceIconMap: Record<string, typeof GithubLogo> = {
|
||||
managed: PuzzlePiece,
|
||||
};
|
||||
|
||||
const sourceLabel: Record<string, string> = {
|
||||
github: "GitHub",
|
||||
local_path: "Local Path",
|
||||
url: "URL",
|
||||
skills_sh: "skills.sh",
|
||||
managed: "Managed",
|
||||
};
|
||||
function sourceLabel(kind: string, t: TranslationFn): string {
|
||||
switch (kind) {
|
||||
case "github":
|
||||
return t("skill.source.github");
|
||||
case "local_path":
|
||||
return t("skill.source.localPath");
|
||||
case "url":
|
||||
return t("skill.source.url");
|
||||
case "skills_sh":
|
||||
return t("skill.source.skillsSh");
|
||||
case "managed":
|
||||
return t("skill.source.managed");
|
||||
default:
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
|
||||
const trustToneMap: Record<string, string> = {
|
||||
markdown_only: "ready",
|
||||
@@ -36,11 +46,18 @@ const trustToneMap: Record<string, string> = {
|
||||
scripts_executables: "error",
|
||||
};
|
||||
|
||||
const trustLabelMap: Record<string, string> = {
|
||||
markdown_only: "Markdown only",
|
||||
assets: "Has assets",
|
||||
scripts_executables: "Contains scripts",
|
||||
};
|
||||
function trustLabel(level: string, t: TranslationFn): string {
|
||||
switch (level) {
|
||||
case "markdown_only":
|
||||
return t("skill.trust.markdownOnly");
|
||||
case "assets":
|
||||
return t("skill.trust.assets");
|
||||
case "scripts_executables":
|
||||
return t("skill.trust.scripts");
|
||||
default:
|
||||
return level;
|
||||
}
|
||||
}
|
||||
|
||||
function stripFrontmatter(markdown: string): string {
|
||||
const normalized = markdown.replace(/\r\n/g, "\n");
|
||||
@@ -50,8 +67,8 @@ function stripFrontmatter(markdown: string): string {
|
||||
return normalized.slice(closing + 5).trim();
|
||||
}
|
||||
|
||||
function shortRef(ref: string | null | undefined): string {
|
||||
if (!ref) return "untracked";
|
||||
function shortRef(ref: string | null | undefined, t: TranslationFn): string {
|
||||
if (!ref) return t("skill.untracked");
|
||||
return ref.length > 7 ? ref.slice(0, 7) : ref;
|
||||
}
|
||||
|
||||
@@ -68,6 +85,7 @@ export function SkillDetail({
|
||||
onDeleted: () => void;
|
||||
onRefresh: () => void;
|
||||
}) {
|
||||
const t = useT();
|
||||
const addToast = useAppStore((s) => s.addToast);
|
||||
const selectedSkillFilePath = useAppStore((s) => s.selectedSkillFilePath);
|
||||
const setSelectedSkillFilePath = useAppStore((s) => s.setSelectedSkillFilePath);
|
||||
@@ -116,12 +134,12 @@ export function SkillDetail({
|
||||
const handleDelete = useCallback(async () => {
|
||||
const result = await window.agentCompany.deleteSkill(companyId, skillId);
|
||||
if (result.ok) {
|
||||
addToast("Skill deleted", "success");
|
||||
addToast(t("skill.deleted"), "success");
|
||||
onDeleted();
|
||||
} else {
|
||||
addToast(result.error.message, "danger");
|
||||
}
|
||||
}, [companyId, skillId, addToast, onDeleted]);
|
||||
}, [companyId, skillId, addToast, onDeleted, t]);
|
||||
|
||||
const handleToggleDir = useCallback((path: string) => {
|
||||
setExpandedDirs((prev) => {
|
||||
@@ -135,7 +153,7 @@ export function SkillDetail({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-8 text-muted-foreground">
|
||||
<div className="text-[13px]">Loading skill...</div>
|
||||
<div className="text-[13px]">{t("skill.loading")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -143,7 +161,7 @@ export function SkillDetail({
|
||||
if (!detail) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-8 text-muted-foreground">
|
||||
<div className="text-[13px]">Skill not found</div>
|
||||
<div className="text-[13px]">{t("skill.notFound")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -186,31 +204,31 @@ export function SkillDetail({
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-[13px]">
|
||||
{/* Source */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Source</span>
|
||||
<Eyebrow>{t("skill.source")}</Eyebrow>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<SourceIcon size={14} className="text-muted-foreground" />
|
||||
<span className="text-foreground">{sourceLabel[detail.sourceType] ?? detail.sourceType}</span>
|
||||
<span className="text-foreground">{sourceLabel(detail.sourceType, t)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Trust level */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Trust</span>
|
||||
<Eyebrow>{t("skill.trust")}</Eyebrow>
|
||||
<StatusPill status={trustToneMap[detail.trustLevel] ?? "default"} />
|
||||
<span className="text-foreground">{trustLabelMap[detail.trustLevel] ?? detail.trustLevel}</span>
|
||||
<span className="text-foreground">{trustLabel(detail.trustLevel, t)}</span>
|
||||
</div>
|
||||
|
||||
{/* Compatibility */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Compat</span>
|
||||
<Eyebrow>{t("skill.compat")}</Eyebrow>
|
||||
<span className="text-foreground">{detail.compatibility || "all"}</span>
|
||||
</div>
|
||||
|
||||
{/* Ref (for GitHub sources) */}
|
||||
{isGitHubSource ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Ref</span>
|
||||
<span className="font-mono text-[12px] text-foreground">{shortRef(detail.sourceRef)}</span>
|
||||
<Eyebrow>{t("skill.ref")}</Eyebrow>
|
||||
<span className="font-mono text-[12px] text-foreground">{shortRef(detail.sourceRef, t)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -219,17 +237,17 @@ export function SkillDetail({
|
||||
{isGitHubSource ? (
|
||||
<div className="flex flex-wrap items-center gap-2 text-[13px]">
|
||||
<ActionButton
|
||||
label="Check for updates"
|
||||
label={t("skill.checkForUpdates")}
|
||||
icon={ArrowsClockwise}
|
||||
loading={checkingUpdate}
|
||||
onClick={() => void handleCheckUpdate()}
|
||||
/>
|
||||
{updateStatus?.hasUpdate ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-accent px-2.5 py-0.5 text-[12px] font-medium text-primary">
|
||||
Update available: {shortRef(updateStatus.currentRef)} → {shortRef(updateStatus.latestRef)}
|
||||
{t("skill.updateAvailable")}: {shortRef(updateStatus.currentRef, t)} → {shortRef(updateStatus.latestRef, t)}
|
||||
</span>
|
||||
) : updateStatus && !updateStatus.hasUpdate ? (
|
||||
<span className="text-[12px] text-muted-foreground">Up to date</span>
|
||||
<span className="text-[12px] text-muted-foreground">{t("skill.upToDate")}</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -250,7 +268,7 @@ export function SkillDetail({
|
||||
onSelectPath={setSelectedSkillFilePath}
|
||||
/>
|
||||
) : (
|
||||
<p className="px-3 py-2 text-[12px] text-muted-foreground">No files</p>
|
||||
<p className="px-3 py-2 text-[12px] text-muted-foreground">{t("skill.noFiles")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -276,11 +294,11 @@ export function SkillDetail({
|
||||
) : selectedFileEntry ? (
|
||||
<div className="rounded-lg bg-card p-4 text-[13px] text-muted-foreground">
|
||||
<p className="font-medium text-foreground">{selectedFileEntry.path}</p>
|
||||
<p className="mt-1">Kind: {selectedFileEntry.kind}</p>
|
||||
<p className="mt-1 text-[12px] italic">File content preview is not available for non-SKILL.md files.</p>
|
||||
<p className="mt-1">{t("skill.kind")}: {selectedFileEntry.kind}</p>
|
||||
<p className="mt-1 text-[12px] italic">{t("skill.previewUnavailable")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[13px] text-muted-foreground">Select a file to view its details.</p>
|
||||
<p className="text-[13px] text-muted-foreground">{t("skill.selectFile")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,9 +306,7 @@ export function SkillDetail({
|
||||
|
||||
{/* Agent assignments section */}
|
||||
<div className="border-t border-border px-5 py-4">
|
||||
<h3 className="text-[11px] font-semibold uppercase tracking-[0.08em] text-primary mb-3">
|
||||
Agent Assignments
|
||||
</h3>
|
||||
<Eyebrow tone="primary" className="mb-3">{t("skill.agentAssignments")}</Eyebrow>
|
||||
<SkillAgentToggles mode="skill" companyId={companyId} skillId={skillId} agents={agents} />
|
||||
</div>
|
||||
|
||||
@@ -299,21 +315,21 @@ export function SkillDetail({
|
||||
{confirmingDelete ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-[13px] text-destructive">
|
||||
Delete “{detail.name}”? This cannot be undone.
|
||||
{t("skill.deleteConfirm").replace("{name}", detail.name)}
|
||||
</p>
|
||||
<ActionButton
|
||||
label="Confirm Delete"
|
||||
label={t("skill.confirmDelete")}
|
||||
tone="danger"
|
||||
onClick={() => void handleDelete()}
|
||||
/>
|
||||
<ActionButton
|
||||
label="Cancel"
|
||||
label={t("skill.cancel")}
|
||||
onClick={() => setConfirmingDelete(false)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ActionButton
|
||||
label="Delete Skill"
|
||||
label={t("skill.deleteSkill")}
|
||||
tone="danger"
|
||||
icon={Trash}
|
||||
onClick={() => setConfirmingDelete(true)}
|
||||
|
||||
@@ -7,16 +7,10 @@ import {
|
||||
} from "../ui/icons";
|
||||
import { Modal, ActionButton, Input } from "../ui";
|
||||
import { useAppStore } from "../../state/store";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -41,6 +35,13 @@ export function SkillImportModal({
|
||||
companyId: string;
|
||||
onImported: () => void;
|
||||
}) {
|
||||
const t = useT();
|
||||
const tabs: { id: TabId; label: string; icon: typeof GithubLogo }[] = [
|
||||
{ id: "github", label: t("skillImport.tabGithub"), icon: GithubLogo },
|
||||
{ id: "local", label: t("skillImport.tabLocal"), icon: FolderSimple },
|
||||
{ id: "url", label: t("skillImport.tabUrl"), icon: Link },
|
||||
{ id: "skills_sh", label: t("skillImport.tabSkillsSh"), icon: Globe },
|
||||
];
|
||||
const addToast = useAppStore((s) => s.addToast);
|
||||
const [activeTab, setActiveTab] = useState<TabId>("github");
|
||||
const [source, setSource] = useState("");
|
||||
@@ -90,28 +91,28 @@ export function SkillImportModal({
|
||||
const result = await window.agentCompany.importSkill(companyId, trimmed);
|
||||
if (result.ok) {
|
||||
const count = result.data.length;
|
||||
addToast(`Imported ${count} skill${count !== 1 ? "s" : ""} successfully`);
|
||||
addToast(t("skillImport.imported").replace("{n}", String(count)));
|
||||
onImported();
|
||||
handleClose();
|
||||
} else {
|
||||
addToast(result.error.message, "danger");
|
||||
}
|
||||
} catch (error) {
|
||||
addToast(error instanceof Error ? error.message : "Import failed", "danger");
|
||||
addToast(error instanceof Error ? error.message : t("skillImport.failed"), "danger");
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
}, [source, companyId, addToast, onImported, handleClose]);
|
||||
}, [source, companyId, addToast, onImported, handleClose, t]);
|
||||
|
||||
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",
|
||||
github: t("skillImport.placeholderGithub"),
|
||||
local: t("skillImport.placeholderLocal"),
|
||||
url: t("skillImport.placeholderUrl"),
|
||||
skills_sh: t("skillImport.placeholderSkillsSh"),
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} title="Import Skill" onClose={handleClose} width="560px">
|
||||
<Modal open={open} title={t("skillImport.title")} onClose={handleClose} width="560px">
|
||||
<div className="space-y-5">
|
||||
{/* Tab bar */}
|
||||
<div className="flex gap-1 border-b border-border">
|
||||
@@ -146,7 +147,7 @@ export function SkillImportModal({
|
||||
<div className="flex-1">
|
||||
<Input value={source} onChange={setSource} placeholder={placeholder.local} />
|
||||
</div>
|
||||
<ActionButton label="Browse" onClick={() => void handleBrowse()} />
|
||||
<ActionButton label={t("skillImport.browse")} onClick={() => void handleBrowse()} />
|
||||
</div>
|
||||
) : (
|
||||
<Input value={source} onChange={setSource} placeholder={placeholder[activeTab]} />
|
||||
@@ -154,16 +155,16 @@ export function SkillImportModal({
|
||||
|
||||
{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.
|
||||
{t("skillImport.skillsShHelp")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<ActionButton label="Cancel" onClick={handleClose} />
|
||||
<ActionButton label={t("skillImport.cancel")} onClick={handleClose} />
|
||||
<ActionButton
|
||||
label="Import"
|
||||
label={t("skillImport.import")}
|
||||
tone="accent"
|
||||
onClick={() => void handleImport()}
|
||||
loading={isImporting}
|
||||
|
||||
Reference in new issue
Block a user