diff --git a/src-tauri/src/workspace/files.rs b/src-tauri/src/workspace/files.rs index fca852ea6..d87144acb 100644 --- a/src-tauri/src/workspace/files.rs +++ b/src-tauri/src/workspace/files.rs @@ -109,31 +109,24 @@ fn seed_templates(templates_dir: &PathBuf) -> Result<(), String> { let defaults = vec![ WorkspaceTemplate { - id: "tmpl_understand_workspace".to_string(), - title: "Understand this workspace".to_string(), - description: "Explains local vs global tools".to_string(), - prompt: "Explain how this workspace is configured and what tools are available locally. Be concise and actionable.".to_string(), + id: "tmpl_interact_with_files".to_string(), + title: "Learn to interact with files".to_string(), + description: "Safe, practical file workflows".to_string(), + prompt: "Show me how to interact with files in this workspace. Include safe examples for reading, summarizing, and editing.".to_string(), created_at: now_ms(), }, WorkspaceTemplate { - id: "tmpl_create_skill".to_string(), - title: "Create a new skill".to_string(), - description: "Guide to adding capabilities".to_string(), - prompt: "I want to create a new skill for this workspace. Guide me through it.".to_string(), + id: "tmpl_learn_skills".to_string(), + title: "Learn about skills".to_string(), + description: "How skills work and how to create your own".to_string(), + prompt: "Explain what skills are, how to use them, and how to create a new skill for this workspace.".to_string(), created_at: now_ms(), }, WorkspaceTemplate { - id: "tmpl_run_scheduled_task".to_string(), - title: "Run a scheduled task".to_string(), - description: "Demo of the scheduler plugin".to_string(), - prompt: "Show me how to schedule a task to run every morning.".to_string(), - created_at: now_ms(), - }, - WorkspaceTemplate { - id: "tmpl_task_to_template".to_string(), - title: "Turn task into template".to_string(), - description: "Save workflow for later".to_string(), - prompt: "Help me turn the last task into a reusable template.".to_string(), + id: "tmpl_learn_plugins".to_string(), + title: "Learn about plugins".to_string(), + description: "What plugins are and how to install them".to_string(), + prompt: "Explain what plugins are and how to install them in this workspace.".to_string(), created_at: now_ms(), }, ]; diff --git a/src/App.tsx b/src/App.tsx index a18daba26..7e7f75758 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1659,7 +1659,7 @@ export default function App() { openTemplateModal, runTemplate, deleteTemplate, - refreshSkills: () => refreshSkills().catch(() => undefined), + refreshSkills: (options?: { force?: boolean }) => refreshSkills(options).catch(() => undefined), refreshPlugins: (scopeOverride?: PluginScope) => refreshPlugins(scopeOverride).catch(() => undefined), skills: skills(), diff --git a/src/app/constants.ts b/src/app/constants.ts index b5249b1b5..c08233026 100644 --- a/src/app/constants.ts +++ b/src/app/constants.ts @@ -13,45 +13,10 @@ export const DEFAULT_MODEL: ModelRef = { export const CURATED_PACKAGES: CuratedPackage[] = [ { - name: "OpenPackage Essentials", - source: "essentials", - description: "Starter rules, commands, and skills from the OpenPackage registry.", - tags: ["registry", "starter"], - installable: true, - }, - { - name: "Claude Code Plugins", - source: "github:anthropics/claude-code", - description: "Official Claude Code plugin pack from GitHub.", - tags: ["github", "claude"], - installable: true, - }, - { - name: "Claude Code Commit Commands", - source: "github:anthropics/claude-code#subdirectory=plugins/commit-commands", - description: "Commit message helper commands (Claude Code plugin).", - tags: ["github", "workflow"], - installable: true, - }, - { - name: "Awesome OpenPackage", - source: "git:https://github.com/enulus/awesome-openpackage.git", - description: "Community collection of OpenPackage examples and templates.", - tags: ["community"], - installable: true, - }, - { - name: "Notion CRM Skill", - source: "github:different-ai/openwork-skills#subdirectory=manage-crm-notion", - description: "Set up a Notion CRM with pipelines, contacts, and follow-ups.", - tags: ["notion", "crm", "demo"], - installable: true, - }, - { - name: "Awesome Claude Skills", - source: "https://github.com/ComposioHQ/awesome-claude-skills", - description: "Curated list of Claude skills and prompts (not an OpenPackage yet).", - tags: ["community", "list"], + name: "Notion CRM Enrichment Skills", + source: "https://github.com/different-ai/notion-crm-enrichment/tree/main/.claude/skills", + description: "Enrich Notion CRM data with ready-made skills.", + tags: ["notion", "crm", "skills"], installable: false, }, ]; diff --git a/src/app/extensions.ts b/src/app/extensions.ts index 32da15609..cfbc4d5b2 100644 --- a/src/app/extensions.ts +++ b/src/app/extensions.ts @@ -39,12 +39,7 @@ export function createExtensionsStore(options: { const [openPackageSource, setOpenPackageSource] = createSignal(""); const [packageSearch, setPackageSearch] = createSignal(""); - const skillDocFallbacks: Record = { - "workspace-guide": "Workspace guide that introduces OpenWork and suggests first steps.", - "manage-crm-notion": "Set up a Notion CRM with pipelines, contacts, and follow-ups.", - }; - const failedSkillDocs = new Set(); - const skillDocKey = (root: string, name: string) => `${root}::${name}`; + const formatSkillPath = (location: string) => location.replace(/[/\\]SKILL\.md$/i, ""); const [pluginScope, setPluginScope] = createSignal("project"); const [pluginConfig, setPluginConfig] = createSignal(null); @@ -61,6 +56,8 @@ export function createExtensionsStore(options: { let refreshPluginsInFlight = false; let refreshSkillsAborted = false; let refreshPluginsAborted = false; + let skillsLoaded = false; + let skillsRoot = ""; const isPluginInstalledByName = (pluginName: string, aliases: string[] = []) => isPluginInstalled(pluginList(), pluginName, aliases); @@ -69,11 +66,29 @@ export function createExtensionsStore(options: { loadPluginsFromConfigHelpers(config, setPluginList, (message) => setPluginStatus(message)); }; - async function refreshSkills() { + async function refreshSkills(optionsOverride?: { force?: boolean }) { const c = options.client(); - if (!c) return; + if (!c) { + setSkills([]); + setSkillsStatus("Connect to a host to load skills."); + return; + } + + const root = options.activeWorkspaceRoot().trim(); + if (!root) { + setSkills([]); + setSkillsStatus("Pick a workspace folder first."); + return; + } + + if (root !== skillsRoot) { + skillsLoaded = false; + } + + if (!optionsOverride?.force && skillsLoaded) { + return; + } - // Skip if already in flight if (refreshSkillsInFlight) { return; } @@ -86,65 +101,40 @@ export function createExtensionsStore(options: { if (refreshSkillsAborted) return; - const nodes = unwrap( - await c.file.list({ directory: options.activeWorkspaceRoot().trim(), path: ".opencode/skill" }), - ); - - if (refreshSkillsAborted) return; - - const dirs = nodes.filter((n) => n.type === "directory" && !n.ignored); - - const next: SkillCard[] = []; - const root = options.activeWorkspaceRoot().trim(); - - for (const dir of dirs) { - if (refreshSkillsAborted) return; - - let description: string | undefined; - const fallback = skillDocFallbacks[dir.name]; - const docKey = skillDocKey(root, dir.name); - - if (fallback && failedSkillDocs.has(docKey)) { - description = fallback; - next.push({ name: dir.name, path: dir.path, description }); - continue; - } - - try { - const skillDoc = unwrap( - await c.file.read({ - directory: root, - path: `.opencode/skill/${dir.name}/SKILL.md`, - }), - ); - - if (skillDoc.type === "text") { - const lines = skillDoc.content.split("\n"); - const first = lines - .map((l) => l.trim()) - .filter((l) => l && !l.startsWith("#")) - .slice(0, 2) - .join(" "); - if (first) { - description = first; - } - } - } catch { - if (fallback) { - failedSkillDocs.add(docKey); - description = fallback; - } - } - - next.push({ name: dir.name, path: dir.path, description }); + const rawClient = c as unknown as { _client?: { get: (input: { url: string }) => Promise } }; + if (!rawClient._client) { + throw new Error("OpenCode client unavailable."); } + const result = await rawClient._client.get({ url: "/skill" }); + if (result?.data === undefined) { + const err = result?.error; + const message = + err instanceof Error ? err.message : typeof err === "string" ? err : "Failed to load skills"; + throw new Error(message); + } + const data = result.data as Array<{ + name: string; + description: string; + location: string; + }>; + if (refreshSkillsAborted) return; + const next: SkillCard[] = Array.isArray(data) + ? data.map((entry) => ({ + name: entry.name, + description: entry.description, + path: formatSkillPath(entry.location), + })) + : []; + setSkills(next); if (!next.length) { - setSkillsStatus("No skills found in .opencode/skill"); + setSkillsStatus("No skills found yet."); } + skillsLoaded = true; + skillsRoot = root; } catch (e) { if (refreshSkillsAborted) return; setSkills([]); @@ -329,7 +319,7 @@ export function createExtensionsStore(options: { } } - await refreshSkills(); + await refreshSkills({ force: true }); } catch (e) { const message = e instanceof Error ? e.message : String(e); options.setError(addOpencodeCacheHint(message)); @@ -382,7 +372,7 @@ export function createExtensionsStore(options: { options.markReloadRequired("skills"); } - await refreshSkills(); + await refreshSkills({ force: true }); } catch (e) { const message = e instanceof Error ? e.message : "Unknown error"; options.setError(addOpencodeCacheHint(message)); diff --git a/src/app/system-state.ts b/src/app/system-state.ts index a5d5a16af..f70e7f391 100644 --- a/src/app/system-state.ts +++ b/src/app/system-state.ts @@ -26,7 +26,7 @@ export function createSystemState(options: { sessions: Accessor; sessionStatusById: Accessor>; refreshPlugins: (scopeOverride?: PluginScope) => Promise; - refreshSkills: () => Promise; + refreshSkills: (options?: { force?: boolean }) => Promise; setProviders: (value: Provider[]) => void; setProviderDefaults: (value: Record) => void; setProviderConnectedIds: (value: string[]) => void; @@ -229,7 +229,7 @@ export function createSystemState(options: { } await options.refreshPlugins("project").catch(() => undefined); - await options.refreshSkills().catch(() => undefined); + await options.refreshSkills({ force: true }).catch(() => undefined); if (options.notion) { let nextStatus = options.notion.status(); diff --git a/src/app/workspace.ts b/src/app/workspace.ts index a6715470a..0ac789b80 100644 --- a/src/app/workspace.ts +++ b/src/app/workspace.ts @@ -64,7 +64,7 @@ export function createWorkspaceStore(options: { setSessionStatusById: (value: Record) => void; defaultModel: () => any; modelVariant: () => string | null; - refreshSkills: () => Promise; + refreshSkills: (options?: { force?: boolean }) => Promise; refreshPlugins: () => Promise; engineSource: () => "path" | "sidecar"; setEngineSource: (value: "path" | "sidecar") => void; @@ -238,60 +238,7 @@ export function createWorkspaceStore(options: { options.setPendingPermissions([]); options.setSessionStatusById({}); - try { - if (isTauriRuntime() && activeWorkspaceRoot().trim()) { - const wsRoot = activeWorkspaceRoot().trim(); - const storedKey = `openwork.welcomeSessionCreated:${wsRoot}`; - - let already = false; - try { - already = window.localStorage.getItem(storedKey) === "1"; - } catch { - // ignore - } - - if (!already) { - const session = unwrap( - await nextClient.session.create({ directory: wsRoot, title: "Welcome to OpenWork" }), - ); - await nextClient.session.promptAsync({ - directory: wsRoot, - sessionID: session.id, - model: options.defaultModel(), - variant: options.modelVariant() ?? undefined, - parts: [ - { - type: "text", - text: - "Give a short, welcoming overview of this workspace and how to use OpenWork. If a workspace guide skill is available, use it. Avoid CLI language or raw file paths. End with two friendly next actions to try inside OpenWork.", - }, - ], - }); - - try { - window.localStorage.setItem(storedKey, "1"); - } catch { - // ignore - } - - await options.loadSessions(activeWorkspaceRoot().trim()).catch(() => undefined); - - if (session?.id) { - try { - await options.selectSession(session.id); - options.setView("session"); - options.setTab("sessions"); - } catch { - // ignore selection failure - } - } - } - } - } catch { - // ignore onboarding session failures - } - - options.refreshSkills().catch(() => undefined); + options.refreshSkills({ force: true }).catch(() => undefined); if (!options.selectedSessionId()) { options.setView("dashboard"); options.setTab("home"); diff --git a/src/components/CreateWorkspaceModal.tsx b/src/components/CreateWorkspaceModal.tsx index 9f29c2a11..377648014 100644 --- a/src/components/CreateWorkspaceModal.tsx +++ b/src/components/CreateWorkspaceModal.tsx @@ -1,6 +1,6 @@ import { For, Show, createSignal } from "solid-js"; -import { CheckCircle2, FolderPlus, X } from "lucide-solid"; +import { CheckCircle2, FolderPlus, Loader2, X } from "lucide-solid"; import Button from "./Button"; @@ -12,22 +12,18 @@ export default function CreateWorkspaceModal(props: { }) { const [preset, setPreset] = createSignal<"starter" | "automation" | "minimal">("starter"); const [selectedFolder, setSelectedFolder] = createSignal(null); + const [pickingFolder, setPickingFolder] = createSignal(false); const options = () => [ { id: "starter" as const, - name: "Starter", - desc: "Pre-configured with Scheduler & starter templates. Best for general use.", - }, - { - id: "automation" as const, - name: "Automation", - desc: "Optimized for scheduled/background work.", + name: "Starter workspace", + desc: "Preconfigured to show you how to use plugins, templates, and skills.", }, { id: "minimal" as const, - name: "Minimal", - desc: "Empty project. Adds only core config.", + name: "Empty workspace", + desc: "Start with a blank folder and add what you need.", }, ]; @@ -45,9 +41,15 @@ export default function CreateWorkspaceModal(props: { }; const handlePickFolder = async () => { - const next = await props.onPickFolder(); - if (next) { - setSelectedFolder(next); + if (pickingFolder()) return; + setPickingFolder(true); + try { + const next = await props.onPickFolder(); + if (next) { + setSelectedFolder(next); + } + } finally { + setPickingFolder(false); } }; @@ -77,7 +79,10 @@ export default function CreateWorkspaceModal(props: { diff --git a/src/views/DashboardView.tsx b/src/views/DashboardView.tsx index b0c32978e..ca95e23ac 100644 --- a/src/views/DashboardView.tsx +++ b/src/views/DashboardView.tsx @@ -29,7 +29,6 @@ import { Plus, Settings, Server, - Smartphone, } from "lucide-solid"; export type DashboardViewProps = { @@ -81,7 +80,7 @@ export type DashboardViewProps = { resetTemplateDraft?: (scope?: "workspace" | "global") => void; runTemplate: (template: WorkspaceTemplate) => void; deleteTemplate: (templateId: string) => void; - refreshSkills: () => void; + refreshSkills: (options?: { force?: boolean }) => void; refreshPlugins: (scopeOverride?: PluginScope) => void; refreshMcpServers: () => void; skills: SkillCard[]; @@ -284,7 +283,7 @@ export default function DashboardView(props: DashboardViewProps) { }); }); - const navItem = (t: DashboardTab, label: string, icon: any) => { + const navItem = (t: DashboardTab, label: any, icon: any) => { const active = () => props.tab === t; return ( @@ -159,7 +159,7 @@ export default function McpView(props: McpViewProps) {
MCPs
- Connect Model Context Protocol servers to expand what OpenWork can do. + Connect MCP servers to expand what OpenWork can do.
diff --git a/src/views/OnboardingView.tsx b/src/views/OnboardingView.tsx index e034672ee..aa1cf10d9 100644 --- a/src/views/OnboardingView.tsx +++ b/src/views/OnboardingView.tsx @@ -99,7 +99,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
Starter Workspace
- OpenWork will create a ready-to-run folder and get everything set up for you. + A ready-to-run workspace with starter templates and plugins.
{props.developerMode ? props.activeWorkspacePath || "(initializing...)" : "A starter workspace will be created for you."} @@ -115,11 +115,11 @@ export default function OnboardingView(props: OnboardingViewProps) {
- Starter templates ("Understand this workspace", etc.) + Starter templates for files, skills, and plugins
- Add more folders when prompted + This workspace is preconfigured to show you how to use plugins, templates, and skills
diff --git a/src/views/SkillsView.tsx b/src/views/SkillsView.tsx index c41806025..88fd0df80 100644 --- a/src/views/SkillsView.tsx +++ b/src/views/SkillsView.tsx @@ -9,7 +9,7 @@ import { Package, Upload } from "lucide-solid"; export type SkillsViewProps = { busy: boolean; mode: "host" | "client" | null; - refreshSkills: () => void; + refreshSkills: (options?: { force?: boolean }) => void; skills: SkillCard[]; skillsStatus: string | null; openPackageSource: string; @@ -27,7 +27,7 @@ export default function SkillsView(props: SkillsViewProps) {

Skills

-
@@ -87,21 +87,21 @@ export default function SkillsView(props: SkillsViewProps) {
-
Manage CRM in Notion
-
Set up pipelines, contacts, and follow-ups in minutes.
+
Notion CRM Enrichment Skills
+
Add enrichment workflows for contacts, pipelines, and follow-ups.
@@ -169,7 +169,7 @@ export default function SkillsView(props: SkillsViewProps) { when={props.skills.length} fallback={
- No skills detected in `.opencode/skill`. + No skills detected yet.
} >