merge: resolve wave 1 worktree conflicts (keep Phase 02 schema validation)

This commit is contained in:
ZacharyZhang-NY committed 2026-03-26 01:54:08 -04:00
commit 6397c118ea
13 files changed
+685 -1

No files matched your search

+11
View File
@@ -14,6 +14,7 @@ import {
MagnifyingGlass,
Megaphone,
Plus,
PuzzlePiece,
UsersThree,
ShieldCheck,
Trash,
@@ -75,6 +76,7 @@ import { SocialAccountsPanel } from "./components/social";
import { AutomationPanel } from "./components/automation";
import { CommunicationHub } from "./components/communication";
import { HiringPipeline } from "./components/hiring";
import { SkillsSection } from "./components/skills";
import { DocumentCenter, KnowledgeBaseManager, MeetingsManager, SprintManager } from "./components/company-operations";
import { ActionButton, ErrorBoundary, ListFrame, ListRow, Modal, Panel, SectionButton, Select, StatusPill, useFocusTrap, useReducedMotion } from "./components/ui";
import ToastContainer from "./components/Toast";
@@ -94,6 +96,7 @@ const sidebarNavDefs: NavItemDef[] = [
{ id: "agents", labelKey: "nav.agents", icon: UsersThree },
{ id: "tasks", labelKey: "nav.work", icon: ListChecks },
{ id: "communication", labelKey: "nav.communication", icon: ChatCircle },
{ id: "skills", labelKey: "nav.skills", icon: PuzzlePiece },
{ id: "documents", labelKey: "nav.documents", icon: FileText },
{ id: "approvals", labelKey: "nav.approvals", icon: ShieldCheck },
{ id: "social", labelKey: "nav.social", icon: Megaphone },
@@ -181,6 +184,10 @@ export function App() {
store.setSocialAccounts([]);
store.setBrowserActions([]);
store.setBadges(null);
store.setSkills([]);
store.setSelectedSkillId(null);
store.setSelectedSkillFilePath("SKILL.md");
store.setSkillImportModalOpen(false);
}, []);
const refresh = useCallback(async () => {
@@ -1193,6 +1200,10 @@ export function App() {
/>
)}
{section === "skills" && currentCompany ? (
<SkillsSection companyId={currentCompany.id} agents={companyAgents} onRefresh={refresh} />
) : null}
{section === "automation" && currentCompany ? (
<AutomationPanel
key={currentCompany.id}
@@ -0,0 +1,136 @@
import {
CaretDown,
CaretRight,
FileCode,
FileText,
FolderOpen,
FolderSimple,
} from "@phosphor-icons/react";
import type { FileInventoryEntry } from "@shared/types";
export type SkillTreeNode = {
name: string;
path: string | null;
kind: "dir" | "file";
fileKind?: FileInventoryEntry["kind"];
children: SkillTreeNode[];
};
const BASE_INDENT = 16;
const STEP_INDENT = 24;
export function buildTree(entries: FileInventoryEntry[]): SkillTreeNode[] {
const root: SkillTreeNode = { name: "", path: null, kind: "dir", children: [] };
for (const entry of entries) {
const segments = entry.path.split("/").filter(Boolean);
let current = root;
let currentPath = "";
for (const [index, segment] of segments.entries()) {
currentPath = currentPath ? `${currentPath}/${segment}` : segment;
const isLeaf = index === segments.length - 1;
let next = current.children.find((child) => child.name === segment);
if (!next) {
next = {
name: segment,
path: isLeaf ? entry.path : currentPath,
kind: isLeaf ? "file" : "dir",
fileKind: isLeaf ? entry.kind : undefined,
children: [],
};
current.children.push(next);
}
current = next;
}
}
function sortNode(node: SkillTreeNode) {
node.children.sort((left, right) => {
if (left.kind !== right.kind) return left.kind === "dir" ? -1 : 1;
if (left.name === "SKILL.md") return -1;
if (right.name === "SKILL.md") return 1;
return left.name.localeCompare(right.name);
});
node.children.forEach(sortNode);
}
sortNode(root);
return root.children;
}
function fileIcon(kind: FileInventoryEntry["kind"] | undefined) {
if (kind === "script" || kind === "reference") return FileCode;
return FileText;
}
export function SkillFileTree({
nodes,
selectedPath,
expandedDirs,
onToggleDir,
onSelectPath,
depth = 0,
}: {
nodes: SkillTreeNode[];
selectedPath: string;
expandedDirs: Set<string>;
onToggleDir: (path: string) => void;
onSelectPath: (path: string) => void;
depth?: number;
}) {
return (
<div>
{nodes.map((node) => {
const expanded = node.kind === "dir" && node.path ? expandedDirs.has(node.path) : false;
if (node.kind === "dir") {
const FolderIcon = expanded ? FolderOpen : FolderSimple;
const ChevronIcon = expanded ? CaretDown : CaretRight;
return (
<div key={node.path ?? node.name}>
<button
type="button"
className="flex min-h-9 w-full items-center gap-2 text-left text-[13px] text-[color:var(--muted)] hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }}
onClick={() => node.path && onToggleDir(node.path)}
>
<FolderIcon size={14} className="shrink-0" />
<span className="min-w-0 truncate">{node.name}</span>
<ChevronIcon size={12} className="ml-auto mr-3 shrink-0 opacity-60" />
</button>
{expanded && (
<SkillFileTree
nodes={node.children}
selectedPath={selectedPath}
expandedDirs={expandedDirs}
onToggleDir={onToggleDir}
onSelectPath={onSelectPath}
depth={depth + 1}
/>
)}
</div>
);
}
const FileIcon = fileIcon(node.fileKind);
const isSelected = node.path === selectedPath;
return (
<button
key={node.path ?? node.name}
type="button"
className={`flex min-h-9 w-full items-center gap-2 text-left text-[13px] hover:bg-[color:var(--panel-soft)] ${
isSelected
? "text-[color:var(--text)] font-medium"
: "text-[color:var(--muted)]"
}`}
style={{ paddingLeft: `${BASE_INDENT + depth * STEP_INDENT}px` }}
onClick={() => node.path && onSelectPath(node.path)}
>
<FileIcon size={14} className="shrink-0" />
<span className="min-w-0 truncate">{node.name}</span>
</button>
);
})}
</div>
);
}
@@ -0,0 +1,184 @@
import { useState, useCallback } from "react";
import {
FolderSimple,
GithubLogo,
Globe,
Link,
} from "@phosphor-icons/react";
import { Modal, ActionButton } 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-[color:var(--line)]">
{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(--accent)] text-[color:var(--text)]"
: "text-[color:var(--muted)] hover:text-[color:var(--text)]"
}`}
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">
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={source}
onChange={(e) => setSource(e.target.value)}
placeholder={placeholder.local}
/>
<ActionButton label="Browse" onClick={() => void handleBrowse()} />
</div>
) : (
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[14px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={source}
onChange={(e) => setSource(e.target.value)}
placeholder={placeholder[activeTab]}
/>
)}
{activeTab === "skills_sh" && (
<p className="text-[12px] text-[color:var(--muted)]">
Enter a skills.sh install command (e.g., <code className="rounded bg-[color:var(--panel-soft)] 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>
);
}
@@ -0,0 +1,135 @@
import { useState, useCallback } from "react";
import {
CaretDown,
CaretRight,
FolderSimple,
GithubLogo,
Globe,
Link,
PuzzlePiece,
} from "@phosphor-icons/react";
import type { SkillRecord } from "@shared/types";
import { SkillFileTree, buildTree } from "./SkillFileTree";
const sourceIconMap: Record<string, typeof GithubLogo> = {
github: GithubLogo,
local_path: FolderSimple,
url: Link,
skills_sh: Globe,
managed: PuzzlePiece,
};
export function SkillList({
skills,
selectedSkillId,
skillFilter,
onSelectSkill,
}: {
skills: SkillRecord[];
selectedSkillId: string | null;
skillFilter: string;
onSelectSkill: (id: string) => void;
}) {
const [expandedSkillId, setExpandedSkillId] = useState<string | null>(null);
const [expandedDirs, setExpandedDirs] = useState<Record<string, Set<string>>>({});
const [selectedPaths, setSelectedPaths] = useState<Record<string, string>>({});
const filteredSkills = skills.filter((skill) => {
const haystack = `${skill.name} ${skill.key} ${skill.slug}`.toLowerCase();
return haystack.includes(skillFilter.toLowerCase());
});
const handleToggleSkill = useCallback((skillId: string) => {
setExpandedSkillId((prev) => (prev === skillId ? null : skillId));
}, []);
const handleToggleDir = useCallback((skillId: string, path: string) => {
setExpandedDirs((prev) => {
const current = prev[skillId] ?? new Set<string>();
const next = new Set(current);
if (next.has(path)) {
next.delete(path);
} else {
next.add(path);
}
return { ...prev, [skillId]: next };
});
}, []);
const handleSelectPath = useCallback((skillId: string, path: string) => {
setSelectedPaths((prev) => ({ ...prev, [skillId]: path }));
}, []);
if (filteredSkills.length === 0) {
return (
<div className="px-4 py-6 text-[13px] text-[color:var(--muted)]">
No skills match this filter.
</div>
);
}
return (
<div>
{filteredSkills.map((skill) => {
const isExpanded = expandedSkillId === skill.id;
const isSelected = skill.id === selectedSkillId;
const tree = buildTree(skill.fileInventory);
const SourceIcon = sourceIconMap[skill.sourceType] ?? PuzzlePiece;
const dirState = expandedDirs[skill.id] ?? new Set<string>();
const filePath = selectedPaths[skill.id] ?? "SKILL.md";
return (
<div key={skill.id} className="border-b border-[color:var(--line)]">
<div
className={`group flex items-center gap-2 px-3 py-1.5 hover:bg-[color:var(--panel-soft)] ${
isSelected ? "text-[color:var(--text)]" : "text-[color:var(--muted)]"
}`}
>
<SourceIcon
size={14}
className="shrink-0 opacity-75 transition-opacity group-hover:opacity-100"
aria-hidden="true"
/>
<button
type="button"
className="min-w-0 flex-1 text-left"
onClick={() => onSelectSkill(skill.id)}
>
<span className="text-[13px] font-medium leading-5 [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:3]">
{skill.name}
</span>
</button>
<button
type="button"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[4px] text-[color:var(--muted)] opacity-60 transition-opacity hover:bg-[color:var(--panel-soft)] hover:opacity-100"
onClick={() => handleToggleSkill(skill.id)}
aria-label={isExpanded ? "Collapse file tree" : "Expand file tree"}
>
{isExpanded ? <CaretDown size={12} /> : <CaretRight size={12} />}
</button>
</div>
<div
className="overflow-hidden transition-[grid-template-rows] duration-200"
style={{
display: "grid",
gridTemplateRows: isExpanded ? "1fr" : "0fr",
}}
>
<div className="min-h-0">
{tree.length > 0 && (
<SkillFileTree
nodes={tree}
selectedPath={filePath}
expandedDirs={dirState}
onToggleDir={(path) => handleToggleDir(skill.id, path)}
onSelectPath={(path) => handleSelectPath(skill.id, path)}
/>
)}
</div>
</div>
</div>
);
})}
</div>
);
}
@@ -0,0 +1,161 @@
import { useCallback, useEffect, useState } from "react";
import {
MagnifyingGlass,
Plus,
PuzzlePiece,
} from "@phosphor-icons/react";
import type { AgentRecord } from "@shared/types";
import { useAppStore } from "../../state/store";
import { EmptyState } from "../ui";
import { SkillList } from "./SkillList";
import { SkillImportModal } from "./SkillImportModal";
export function SkillsSection({
companyId,
agents: _agents,
onRefresh: _onRefresh,
}: {
companyId: string;
agents: AgentRecord[];
onRefresh: () => void;
}) {
const skills = useAppStore((s) => s.skills);
const setSkills = useAppStore((s) => s.setSkills);
const selectedSkillId = useAppStore((s) => s.selectedSkillId);
const setSelectedSkillId = useAppStore((s) => s.setSelectedSkillId);
const skillImportModalOpen = useAppStore((s) => s.skillImportModalOpen);
const setSkillImportModalOpen = useAppStore((s) => s.setSkillImportModalOpen);
const addToast = useAppStore((s) => s.addToast);
const [skillFilter, setSkillFilter] = useState("");
const loadSkills = useCallback(async () => {
try {
const result = await window.agentCompany.listSkills(companyId);
if (result.ok) {
setSkills(result.data);
} else {
addToast(result.error.message, "danger");
}
} catch (error) {
addToast(error instanceof Error ? error.message : "Failed to load skills", "danger");
}
}, [companyId, setSkills, addToast]);
useEffect(() => {
void loadSkills();
}, [loadSkills]);
const handleImported = useCallback(() => {
void loadSkills();
}, [loadSkills]);
const handleSelectSkill = useCallback(
(id: string) => {
setSelectedSkillId(id);
},
[setSelectedSkillId],
);
const selectedSkill = skills.find((s) => s.id === selectedSkillId);
return (
<div className="grid h-full grid-cols-1 xl:grid-cols-[19rem_minmax(0,1fr)]">
{/* Left column -- skill list sidebar */}
<aside className="flex flex-col overflow-y-auto border-r border-[color:var(--line)]">
{/* Header */}
<div className="flex shrink-0 items-center gap-3 px-4 py-3">
<h2 className="text-[20px] font-bold tracking-[-0.025em] text-[color:var(--text)]">
Skills
</h2>
{skills.length > 0 && (
<span className="inline-flex h-[20px] min-w-[20px] items-center justify-center rounded-full bg-[color:var(--panel-soft)] px-1.5 text-[11px] font-semibold text-[color:var(--muted-strong)]">
{skills.length}
</span>
)}
<button
type="button"
className="focus-ring ml-auto flex h-7 w-7 items-center justify-center rounded-[6px] text-[color:var(--muted)] transition hover:bg-[color:var(--panel-soft)] hover:text-[color:var(--text)]"
onClick={() => setSkillImportModalOpen(true)}
aria-label="Import skill"
>
<Plus size={16} weight="bold" />
</button>
</div>
{/* Filter input */}
<div className="shrink-0 px-3 pb-2">
<div className="relative">
<MagnifyingGlass
size={14}
className="absolute left-3 top-1/2 -translate-y-1/2 text-[color:var(--muted)]"
aria-hidden="true"
/>
<input
className="focus-ring w-full rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] py-2 pl-8 pr-3 text-[13px] text-[color:var(--text)] placeholder:text-[color:var(--muted)]"
value={skillFilter}
onChange={(e) => setSkillFilter(e.target.value)}
placeholder="Filter skills..."
aria-label="Filter skills"
/>
</div>
</div>
{/* Skill list */}
<div className="flex-1 overflow-y-auto">
{skills.length === 0 ? (
<EmptyState
title="No skills imported"
detail="Import skills from GitHub, a local path, or skills.sh to get started."
action={
<button
type="button"
className="focus-ring inline-flex items-center gap-2 rounded-[8px] border border-transparent bg-[color:var(--accent)] px-4 py-2.5 text-[12px] font-bold text-[color:var(--text-on-accent)] shadow-[0_1px_3px_rgba(74,93,122,0.35)] transition hover:bg-[color:var(--accent-hover)]"
onClick={() => setSkillImportModalOpen(true)}
>
<Plus size={14} weight="bold" />
Import Skill
</button>
}
/>
) : (
<SkillList
skills={skills}
selectedSkillId={selectedSkillId}
skillFilter={skillFilter}
onSelectSkill={handleSelectSkill}
/>
)}
</div>
</aside>
{/* Right column -- skill detail */}
<div className="flex flex-1 flex-col overflow-y-auto">
{selectedSkill ? (
<div className="flex flex-1 items-center justify-center p-8 text-[color:var(--muted)]">
<div className="text-center">
<PuzzlePiece size={32} className="mx-auto mb-3 opacity-40" />
<p className="text-[14px] font-medium text-[color:var(--text)]">{selectedSkill.name}</p>
<p className="mt-1 text-[13px]">Skill detail pane -- Plan 02</p>
</div>
</div>
) : (
<div className="flex flex-1 items-center justify-center">
<EmptyState
title="Select a skill to view details"
detail="Choose a skill from the list to see its content, file tree, and configuration."
/>
</div>
)}
</div>
{/* Import modal */}
<SkillImportModal
open={skillImportModalOpen}
onClose={() => setSkillImportModalOpen(false)}
companyId={companyId}
onImported={handleImported}
/>
</div>
);
}
+1
View File
@@ -0,0 +1 @@
export { SkillsSection } from "./SkillsSection";
+1
View File
@@ -22,6 +22,7 @@ export const en: Record<string, string> = {
"nav.costs": "Costs",
"nav.activity": "Activity",
"nav.social": "Social Media",
"nav.skills": "Skills",
"nav.connectors": "Connectors",
"nav.workspaces": "Workspaces",
"nav.settings": "Settings",
+1
View File
@@ -22,6 +22,7 @@ export const zh: Record<string, string> = {
"nav.costs": "成本",
"nav.activity": "活动",
"nav.social": "社交媒体",
"nav.skills": "技能",
"nav.connectors": "连接器",
"nav.workspaces": "工作区",
"nav.settings": "设置",
+1
View File
@@ -105,6 +105,7 @@ export const createNavigationSlice: StateCreator<
else if (section === "connectors") state.selectedConnectorId = entityId;
else if (section === "communication")
state.selectedCommunicationEntityId = entityId;
else if (section === "skills") state.selectedSkillId = entityId;
}
}),
});
+42
View File
@@ -0,0 +1,42 @@
import type { StateCreator } from "zustand";
import type { AppState } from "../store";
import type { SkillRecord } from "@shared/types";
export interface SkillSlice {
skills: SkillRecord[];
setSkills: (skills: SkillRecord[]) => void;
selectedSkillId: string | null;
setSelectedSkillId: (id: string | null) => void;
selectedSkillFilePath: string;
setSelectedSkillFilePath: (path: string) => void;
skillImportModalOpen: boolean;
setSkillImportModalOpen: (open: boolean) => void;
}
export const createSkillSlice: StateCreator<
AppState,
[["zustand/immer", never]],
[],
SkillSlice
> = (set) => ({
skills: [],
setSkills: (skills) =>
set((state) => {
state.skills = skills;
}),
selectedSkillId: null,
setSelectedSkillId: (id) =>
set((state) => {
state.selectedSkillId = id;
}),
selectedSkillFilePath: "SKILL.md",
setSelectedSkillFilePath: (path) =>
set((state) => {
state.selectedSkillFilePath = path;
}),
skillImportModalOpen: false,
setSkillImportModalOpen: (open) =>
set((state) => {
state.skillImportModalOpen = open;
}),
});
+4 -1
View File
@@ -16,13 +16,15 @@ import {
type CommunicationSlice,
} from "./slices/communication";
import { createSocialSlice, type SocialSlice } from "./slices/social";
import { createSkillSlice, type SkillSlice } from "./slices/skill";
export type AppState = NavigationSlice &
CompanySlice &
SelectionSlice &
UiSlice &
CommunicationSlice &
SocialSlice;
SocialSlice &
SkillSlice;
export const useAppStore = create<AppState>()(
devtools(
@@ -33,6 +35,7 @@ export const useAppStore = create<AppState>()(
...createUiSlice(...a),
...createCommunicationSlice(...a),
...createSocialSlice(...a),
...createSkillSlice(...a),
})),
{
name: "AgentCompany",
+7
View File
@@ -642,5 +642,12 @@ export type DesktopApi = {
recoveryRetry: (taskId: string, agentId: string, companyId: string) => Promise<DesktopResult<null>>;
recoveryDismiss: (taskId: string) => Promise<DesktopResult<null>>;
recoveryFail: (taskId: string) => Promise<DesktopResult<null>>;
importSkill: (companyId: string, source: string) => Promise<DesktopResult<SkillImportResult[]>>;
listSkills: (companyId: string) => Promise<DesktopResult<SkillRecord[]>>;
getSkillDetail: (companyId: string, skillId: string) => Promise<DesktopResult<SkillRecord>>;
deleteSkill: (companyId: string, skillId: string) => Promise<DesktopResult<void>>;
assignSkillToAgent: (companyId: string, agentId: string, skillId: string) => Promise<DesktopResult<void>>;
removeSkillFromAgent: (companyId: string, agentId: string, skillId: string) => Promise<DesktopResult<void>>;
checkSkillUpdate: (companyId: string, skillId: string) => Promise<DesktopResult<SkillUpdateStatus>>;
subscribe: (callback: (event: DesktopEvent) => void) => () => void;
};
+1
View File
@@ -16,6 +16,7 @@ export type SectionId =
| "activity"
| "workspaces"
| "social"
| "skills"
| "automation"
| "communication"
| "documents"