From e8d642941fd03485d906e2046ecd950dd39e8e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 26 Mar 2026 01:45:32 -0400 Subject: [PATCH] feat(03-01): add skill section type wiring, Zustand slice, i18n, and sidebar nav entry - Add "skills" to SectionId union in shared types - Add SkillRecord, SkillImportResult, SkillUpdateStatus types to shared/types.ts - Create SkillSlice with skills list, selection, file path, and import modal state - Wire SkillSlice into Zustand store composition - Add "nav.skills" i18n keys for English and Chinese - Add Skills sidebar nav entry with PuzzlePiece icon between Communication and Documents - Add section === "skills" rendering route in App.tsx - Clear skill state on company switch - Add skills case to navigateTo in NavigationSlice Co-Authored-By: Claude Opus 4.6 (1M context) --- src/renderer/App.tsx | 11 +++++ .../components/skills/SkillsSection.tsx | 15 +++++++ src/renderer/components/skills/index.ts | 1 + src/renderer/i18n/en.ts | 1 + src/renderer/i18n/zh.ts | 1 + src/renderer/state/slices/navigation.ts | 1 + src/renderer/state/slices/skill.ts | 42 +++++++++++++++++ src/renderer/state/store.ts | 5 ++- src/shared/types.ts | 45 +++++++++++++++++++ 9 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/skills/SkillsSection.tsx create mode 100644 src/renderer/components/skills/index.ts create mode 100644 src/renderer/state/slices/skill.ts diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5fe41304..3a1a822a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -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 ? ( + + ) : null} + {section === "automation" && currentCompany ? ( void; +} + +export function SkillsSection({ companyId: _companyId, agents: _agents, onRefresh: _onRefresh }: SkillsSectionProps) { + return ( +
+

Skills section placeholder -- Task 2

+
+ ); +} diff --git a/src/renderer/components/skills/index.ts b/src/renderer/components/skills/index.ts new file mode 100644 index 00000000..bdb8af01 --- /dev/null +++ b/src/renderer/components/skills/index.ts @@ -0,0 +1 @@ +export { SkillsSection } from "./SkillsSection"; diff --git a/src/renderer/i18n/en.ts b/src/renderer/i18n/en.ts index 5bb1b77a..5de272ca 100644 --- a/src/renderer/i18n/en.ts +++ b/src/renderer/i18n/en.ts @@ -22,6 +22,7 @@ export const en: Record = { "nav.costs": "Costs", "nav.activity": "Activity", "nav.social": "Social Media", + "nav.skills": "Skills", "nav.connectors": "Connectors", "nav.workspaces": "Workspaces", "nav.settings": "Settings", diff --git a/src/renderer/i18n/zh.ts b/src/renderer/i18n/zh.ts index 9a58dca0..64f92143 100644 --- a/src/renderer/i18n/zh.ts +++ b/src/renderer/i18n/zh.ts @@ -22,6 +22,7 @@ export const zh: Record = { "nav.costs": "成本", "nav.activity": "活动", "nav.social": "社交媒体", + "nav.skills": "技能", "nav.connectors": "连接器", "nav.workspaces": "工作区", "nav.settings": "设置", diff --git a/src/renderer/state/slices/navigation.ts b/src/renderer/state/slices/navigation.ts index 9f1addc1..615358fb 100644 --- a/src/renderer/state/slices/navigation.ts +++ b/src/renderer/state/slices/navigation.ts @@ -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; } }), }); diff --git a/src/renderer/state/slices/skill.ts b/src/renderer/state/slices/skill.ts new file mode 100644 index 00000000..e41dcee3 --- /dev/null +++ b/src/renderer/state/slices/skill.ts @@ -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; + }), +}); diff --git a/src/renderer/state/store.ts b/src/renderer/state/store.ts index 36124e8a..be6e8bd1 100644 --- a/src/renderer/state/store.ts +++ b/src/renderer/state/store.ts @@ -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()( devtools( @@ -33,6 +35,7 @@ export const useAppStore = create()( ...createUiSlice(...a), ...createCommunicationSlice(...a), ...createSocialSlice(...a), + ...createSkillSlice(...a), })), { name: "AgentCompany", diff --git a/src/shared/types.ts b/src/shared/types.ts index c3cefc6a..334f415b 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -16,6 +16,7 @@ export type SectionId = | "activity" | "workspaces" | "social" + | "skills" | "automation" | "communication" | "documents" @@ -453,6 +454,50 @@ export interface OnboardingBootstrapResult { runId: string | null; } +// ── Skill types ── + +export type SkillSourceType = "github" | "local_path" | "url" | "skills_sh" | "managed"; +export type TrustLevel = "markdown_only" | "assets" | "scripts_executables"; + +export interface FileInventoryEntry { + path: string; + kind: "skill" | "reference" | "script" | "asset" | "markdown" | "other"; +} + +export interface SkillRecord { + id: string; + companyId: string; + key: string; + slug: string; + name: string; + description: string | null; + markdown: string; + sourceType: SkillSourceType; + sourceLocator: string | null; + sourceRef: string | null; + trustLevel: TrustLevel; + compatibility: string; + fileInventory: FileInventoryEntry[]; + metadata: Record | null; + createdAt: string; + updatedAt: string; +} + +export interface SkillImportResult { + skillId: string; + key: string; + name: string; + isUpdate: boolean; +} + +export interface SkillUpdateStatus { + skillId: string; + key: string; + currentRef: string | null; + latestRef: string | null; + hasUpdate: boolean; +} + export type SocialPlatform = "twitter" | "linkedin" | "instagram" | "facebook" | "reddit" | "youtube" | "tiktok" | "bluesky" | "other"; export type SocialAccountStatus = "active" | "logged_in" | "login_required" | "suspended" | "paused"; export type BrowserActionType = "post" | "reply" | "like" | "follow" | "browse_feed" | "screenshot" | "navigate" | "search";