mirror of
https://github.com/different-ai/openwork
synced 2026-05-10 17:22:05 +02:00
feat: refine onboarding, skills, and MCP alpha UX (#118)
This commit is contained in:
@@ -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(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -39,12 +39,7 @@ export function createExtensionsStore(options: {
|
||||
const [openPackageSource, setOpenPackageSource] = createSignal("");
|
||||
const [packageSearch, setPackageSearch] = createSignal("");
|
||||
|
||||
const skillDocFallbacks: Record<string, string> = {
|
||||
"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<string>();
|
||||
const skillDocKey = (root: string, name: string) => `${root}::${name}`;
|
||||
const formatSkillPath = (location: string) => location.replace(/[/\\]SKILL\.md$/i, "");
|
||||
|
||||
const [pluginScope, setPluginScope] = createSignal<PluginScope>("project");
|
||||
const [pluginConfig, setPluginConfig] = createSignal<OpencodeConfigFile | null>(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<any> } };
|
||||
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));
|
||||
|
||||
@@ -26,7 +26,7 @@ export function createSystemState(options: {
|
||||
sessions: Accessor<Session[]>;
|
||||
sessionStatusById: Accessor<Record<string, string>>;
|
||||
refreshPlugins: (scopeOverride?: PluginScope) => Promise<void>;
|
||||
refreshSkills: () => Promise<void>;
|
||||
refreshSkills: (options?: { force?: boolean }) => Promise<void>;
|
||||
setProviders: (value: Provider[]) => void;
|
||||
setProviderDefaults: (value: Record<string, string>) => 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();
|
||||
|
||||
@@ -64,7 +64,7 @@ export function createWorkspaceStore(options: {
|
||||
setSessionStatusById: (value: Record<string, string>) => void;
|
||||
defaultModel: () => any;
|
||||
modelVariant: () => string | null;
|
||||
refreshSkills: () => Promise<void>;
|
||||
refreshSkills: (options?: { force?: boolean }) => Promise<void>;
|
||||
refreshPlugins: () => Promise<void>;
|
||||
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");
|
||||
|
||||
@@ -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<string | null>(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: {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePickFolder}
|
||||
class="w-full border border-dashed border-zinc-700 bg-zinc-900/50 rounded-xl p-4 text-left transition hover:border-zinc-500"
|
||||
disabled={pickingFolder()}
|
||||
class={`w-full border border-dashed border-zinc-700 bg-zinc-900/50 rounded-xl p-4 text-left transition ${
|
||||
pickingFolder() ? "opacity-70 cursor-wait" : "hover:border-zinc-500"
|
||||
}`.trim()}
|
||||
>
|
||||
<div class="flex items-center gap-3 text-zinc-200">
|
||||
<FolderPlus size={20} class="text-zinc-400" />
|
||||
@@ -85,7 +90,15 @@ export default function CreateWorkspaceModal(props: {
|
||||
<div class="text-sm font-medium text-zinc-100 truncate">{folderLabel()}</div>
|
||||
<div class="text-xs text-zinc-500 font-mono truncate mt-1">{folderSubLabel()}</div>
|
||||
</div>
|
||||
<span class="text-xs text-zinc-500">Change</span>
|
||||
<Show
|
||||
when={pickingFolder()}
|
||||
fallback={<span class="text-xs text-zinc-500">Change</span>}
|
||||
>
|
||||
<span class="flex items-center gap-2 text-xs text-zinc-500">
|
||||
<Loader2 size={12} class="animate-spin" />
|
||||
Opening...
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
@@ -318,7 +317,16 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
{navItem("templates", "Templates", <FileText size={18} />)}
|
||||
{navItem("skills", "Skills", <Package size={18} />)}
|
||||
{navItem("plugins", "Plugins", <Cpu size={18} />)}
|
||||
{navItem("mcp", "MCPs", <Server size={18} />)}
|
||||
{navItem(
|
||||
"mcp",
|
||||
<span class="inline-flex items-center gap-2">
|
||||
MCPs
|
||||
<span class="text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-200">
|
||||
Alpha
|
||||
</span>
|
||||
</span>,
|
||||
<Server size={18} />,
|
||||
)}
|
||||
{navItem("settings", "Settings", <Settings size={18} />)}
|
||||
</nav>
|
||||
</div>
|
||||
@@ -326,12 +334,12 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
<div class="space-y-4">
|
||||
<div class="px-3 py-3 rounded-xl bg-zinc-900/50 border border-zinc-800">
|
||||
<div class="flex items-center gap-2 text-xs font-medium text-zinc-400 mb-2">
|
||||
{props.mode === "host" ? (
|
||||
<Cpu size={12} />
|
||||
) : (
|
||||
<Smartphone size={12} />
|
||||
)}
|
||||
{props.mode === "host" ? "Local Engine" : "Client Mode"}
|
||||
Connection
|
||||
<Show when={props.developerMode}>
|
||||
<span class="text-zinc-600">
|
||||
{props.mode === "host" ? "Local Engine" : "Client Mode"}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
@@ -342,16 +350,18 @@ export default function DashboardView(props: DashboardViewProps) {
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
class={`text-sm font-mono ${
|
||||
class={`text-sm font-medium ${
|
||||
props.clientConnected ? "text-emerald-500" : "text-zinc-500"
|
||||
}`}
|
||||
>
|
||||
{props.clientConnected ? "Connected" : "Disconnected"}
|
||||
{props.clientConnected ? "Connected" : "Not connected"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-[11px] text-zinc-600 font-mono truncate">
|
||||
{props.baseUrl}
|
||||
</div>
|
||||
<Show when={props.developerMode}>
|
||||
<div class="mt-2 text-[11px] text-zinc-600 font-mono truncate">
|
||||
{props.baseUrl}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={props.mode === "host"}>
|
||||
|
||||
@@ -72,7 +72,7 @@ const statusLabel = (status: "connected" | "needs_auth" | "needs_client_registra
|
||||
|
||||
export default function McpView(props: McpViewProps) {
|
||||
const [advancedOpen, setAdvancedOpen] = createSignal(false);
|
||||
const [showDangerousContent, setShowDangerousContent] = createSignal(false);
|
||||
const [showDangerousContent, setShowDangerousContent] = createSignal(true);
|
||||
|
||||
const selectedEntry = createMemo(() =>
|
||||
props.mcpServers.find((entry) => entry.name === props.selectedMcp) ?? null,
|
||||
@@ -108,9 +108,9 @@ export default function McpView(props: McpViewProps) {
|
||||
<section class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-lg font-semibold text-white">Model Context Protocol</h2>
|
||||
<h2 class="text-lg font-semibold text-white">MCP (Alpha)</h2>
|
||||
<p class="text-sm text-zinc-400">
|
||||
MCP servers allow you to easily connect your favorite service and login using your own credentials.
|
||||
MCP servers let you connect services with your own credentials.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function McpView(props: McpViewProps) {
|
||||
<TriangleAlert size={20} class="text-amber-400 shrink-0 mt-0.5" />
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-medium text-amber-200">
|
||||
hey we're currently building this and are chatting with opencode to figure out a bug before this can be pushed live.
|
||||
MCP is in alpha while we harden OAuth with OpenCode.
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<a
|
||||
@@ -132,7 +132,7 @@ export default function McpView(props: McpViewProps) {
|
||||
View issue #9510 on GitHub
|
||||
</a>
|
||||
<p class="text-xs text-zinc-400 leading-relaxed">
|
||||
if you want to fix it or have a look at it feel free to submit a pr and show video for proof the oauth flows works
|
||||
If you want to help, open a PR and include a short video showing the OAuth flow works end to end.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +147,7 @@ export default function McpView(props: McpViewProps) {
|
||||
<Show when={showDangerousContent()} fallback={<ChevronRight size={14} class="group-hover:translate-x-0.5 transition-transform" />}>
|
||||
<ChevronDown size={14} />
|
||||
</Show>
|
||||
dangerously use
|
||||
{showDangerousContent() ? "Hide advanced settings" : "Show advanced settings"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function McpView(props: McpViewProps) {
|
||||
<div>
|
||||
<div class="text-sm font-medium text-white">MCPs</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
Connect Model Context Protocol servers to expand what OpenWork can do.
|
||||
Connect MCP servers to expand what OpenWork can do.
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-500 text-right">
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-white">Starter Workspace</div>
|
||||
<div class="text-xs text-zinc-500">
|
||||
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.
|
||||
</div>
|
||||
<div class={`text-xs ${props.developerMode ? "text-zinc-600 font-mono" : "text-zinc-500"} break-all`}>
|
||||
{props.developerMode ? props.activeWorkspacePath || "(initializing...)" : "A starter workspace will be created for you."}
|
||||
@@ -115,11 +115,11 @@ export default function OnboardingView(props: OnboardingViewProps) {
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-sm text-zinc-300">
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
Starter templates ("Understand this workspace", etc.)
|
||||
Starter templates for files, skills, and plugins
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-sm text-zinc-300">
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-500" />
|
||||
Add more folders when prompted
|
||||
This workspace is preconfigured to show you how to use plugins, templates, and skills
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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) {
|
||||
<section class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-zinc-400 uppercase tracking-wider">Skills</h3>
|
||||
<Button variant="secondary" onClick={props.refreshSkills} disabled={props.busy}>
|
||||
<Button variant="secondary" onClick={() => props.refreshSkills({ force: true })} disabled={props.busy}>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
@@ -87,21 +87,21 @@ export default function SkillsView(props: SkillsViewProps) {
|
||||
<div class="rounded-2xl border border-emerald-500/20 bg-emerald-500/10 p-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-emerald-100">Manage CRM in Notion</div>
|
||||
<div class="text-xs text-emerald-200/80 mt-1">Set up pipelines, contacts, and follow-ups in minutes.</div>
|
||||
<div class="text-sm font-medium text-emerald-100">Notion CRM Enrichment Skills</div>
|
||||
<div class="text-xs text-emerald-200/80 mt-1">Add enrichment workflows for contacts, pipelines, and follow-ups.</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => props.useCuratedPackage({
|
||||
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: "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,
|
||||
})}
|
||||
disabled={props.busy || props.mode !== "host" || !isTauriRuntime()}
|
||||
disabled={props.busy}
|
||||
>
|
||||
Install
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -169,7 +169,7 @@ export default function SkillsView(props: SkillsViewProps) {
|
||||
when={props.skills.length}
|
||||
fallback={
|
||||
<div class="bg-zinc-900/30 border border-zinc-800/50 rounded-2xl p-6 text-sm text-zinc-500">
|
||||
No skills detected in `.opencode/skill`.
|
||||
No skills detected yet.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user