Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions src/components/SkillsCallout/ManualInstall.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react';

import CommandBlock from './CommandBlock';
import { AGENT_INSTALLS } from './agents';
import { AGENT_INSTALLS, agentInstallsFor } from './agents';
import { capturePostHogEvent } from './analytics';
import styles from './styles.module.css';

Expand All @@ -12,11 +12,33 @@ export interface ManualInstallProps {
/**
* Per-agent install steps behind a compact picker. The one-command install
* (InstallCommand) covers every agent; this is the escape hatch for readers who
* want the marketplace flow their agent ships with.
* want the marketplace flow their agent ships with, or who build on a platform
* that imports skills from GitHub instead of running a CLI.
*/
const ManualInstall: React.FC<ManualInstallProps> = ({ framework }) => {
const installs = agentInstallsFor(framework);
const [agentKey, setAgentKey] = useState(AGENT_INSTALLS[0].key);
const agent = AGENT_INSTALLS.find((a) => a.key === agentKey) || AGENT_INSTALLS[0];
// Clamped to an entry the current framework actually offers: the initial key
// comes from the unfiltered list and the selection is not reset when the
// framework changes, so agentKey can name a filtered-out entry. Everything
// below renders from `agent`, including the select's value, so the picker
// can't point at an option that isn't there.
const agent = installs.find((a) => a.key === agentKey) || installs[0];
Comment thread
moritzhartmeier marked this conversation as resolved.
// Grouped entries (the app builders) only exist on some frameworks, so the
// picker stays a flat list everywhere else.
const agents = installs.filter((a) => !a.group);
const groups = installs
.filter((a) => a.group)
.reduce<Record<string, typeof installs>>((acc, a) => {
(acc[a.group!] ||= []).push(a);
return acc;
}, {});
const groupNames = Object.keys(groups);
const renderOption = (a: (typeof installs)[number]) => (
<option key={a.key} value={a.key}>
{a.label}
</option>
);

const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (e) => {
setAgentKey(e.target.value);
Expand All @@ -30,19 +52,26 @@ const ManualInstall: React.FC<ManualInstallProps> = ({ framework }) => {
<div className={styles.manual}>
<div className={styles.manualHeader}>
<label className={styles.manualLabel} htmlFor="skills-agent-picker">
Coding agent
{groupNames.length ? 'Agent or platform' : 'Coding agent'}
</label>
<select
id="skills-agent-picker"
className={styles.manualSelect}
value={agentKey}
value={agent.key}
onChange={handleChange}
>
{AGENT_INSTALLS.map((a) => (
<option key={a.key} value={a.key}>
{a.label}
</option>
))}
{groupNames.length ? (
<>
<optgroup label="Coding agents">{agents.map(renderOption)}</optgroup>
{groupNames.map((name) => (
<optgroup key={name} label={name}>
{groups[name].map(renderOption)}
</optgroup>
))}
</>
) : (
agents.map(renderOption)
)}
</select>
</div>

Expand Down Expand Up @@ -71,6 +100,13 @@ const ManualInstall: React.FC<ManualInstallProps> = ({ framework }) => {
/>
))}
{agent.note && <p className={styles.tabHint}>{agent.note}</p>}
{agent.docs && (
<p className={styles.tabHint}>
<a href={agent.docs.url} target="_blank" rel="noopener noreferrer">
{agent.docs.label} →
</a>
</p>
)}
<p className={styles.tabHint}>
<strong>Updates:</strong> {agent.update}
</p>
Expand Down
49 changes: 49 additions & 0 deletions src/components/SkillsCallout/agents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,35 @@ export interface AgentInstall {
key: string;
/** Label in the agent picker. */
label: string;
/**
* Optgroup in the picker. Ungrouped entries are the coding agents; grouped
* ones are listed after them under this label.
*/
group?: string;
/**
* Framework slugs this entry applies to. Omit for "every framework". AI app
* builders generate web apps, so they only make sense on the Web page.
*/
frameworks?: string[];
/** One-click marketplace install, offered before the commands. */
oneClick?: { url: string; label: string; trackingId: string };
/** Where the commands are typed, e.g. "in Claude Code" or "in a terminal". */
where?: string;
commands?: AgentCommand[];
/** Anything else worth knowing, e.g. how to find the plugin by hand. */
note?: React.ReactNode;
/** The platform's own instructions, linked after the steps. */
docs?: { url: string; label: string };
/**
* How to keep the plugin current once installed. Secondary to the install
* steps, so any command here is inline rather than its own copy block.
*/
update: React.ReactNode;
}

/** Picker group for platforms that install skills through their own UI. */
export const APP_BUILDER_GROUP = 'AI app builders';

export const AGENT_INSTALLS: AgentInstall[] = [
{
key: 'claude-code',
Expand Down Expand Up @@ -153,4 +168,38 @@ export const AGENT_INSTALLS: AgentInstall[] = [
</>
),
},
{
key: 'bolt',
label: 'Bolt',
group: APP_BUILDER_GROUP,
frameworks: ['web'],
where:
'In Bolt, open the plus menu next to the prompt → Skills → Manage skills (inside a project: gear icon → Skills). Then Add skill → From GitHub, paste the repository URL (copy the block below), pick the skill under Skill folder name and click Create.',
commands: [{ command: skillsData.repo, trackingId: 'bolt-github' }],
note: (
<>
The dropdown lists every folder in the repository, so pick the skill by
the name it has in the table above; repeat the import for each skill you
want. Importing from a project's Skills page keeps the skill to that
project, while importing from <strong>Settings → Skills library</strong>{' '}
makes it available to every project in the workspace, switched on per
project.
</>
),
docs: {
url: 'https://support.bolt.new/building/skills#import-skills-from-github',
label: "Bolt's Skills documentation",
},
update: 'Remove the skill and import it again.',
},
];

/**
* Entries to offer for a framework: the coding agents always, plus the app
* builders only where their generated code matches the framework.
*/
export function agentInstallsFor(framework?: string): AgentInstall[] {
return AGENT_INSTALLS.filter(
(a) => !a.frameworks || !framework || a.frameworks.includes(framework),
);
}
33 changes: 30 additions & 3 deletions src/components/SkillsPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import productsData from '@site/src/data/products.json';
import CommandBlock from '../SkillsCallout/CommandBlock';
import InstallCommand from '../SkillsCallout/InstallCommand';
import ManualInstall from '../SkillsCallout/ManualInstall';
import { singleSkillCommand } from '../SkillsCallout/agents';
import {
APP_BUILDER_GROUP,
agentInstallsFor,
singleSkillCommand,
} from '../SkillsCallout/agents';
import { frameworkToSlug } from '../utils/frameworks';
import { withCurrentDocsPath } from '@site/src/constants/docsPaths';
import styles from './styles.module.css';
Expand Down Expand Up @@ -63,6 +67,18 @@ const SkillsPage: React.FC<SkillsPageProps> = ({ framework }) => {
const primarySlug = slugFor('sparkscan') || fwSkills[0]?.slug || skillsData.shared;
const barcodeSlug = slugFor('barcode-capture') || primarySlug;

// AI app builders install skills through their own UI rather than the CLI,
// and only ship web apps — so they are offered on the Web page alone.
// Everywhere else these sections stay agent-only.
const appBuilders = agentInstallsFor(frameworkSlug).filter(
(a) => a.group === APP_BUILDER_GROUP,
);
const appBuilderNames = appBuilders.map((a) => a.label);
const joinNames = (conjunction: string): string =>
appBuilderNames.length > 1
? `${appBuilderNames.slice(0, -1).join(', ')} ${conjunction} ${appBuilderNames[appBuilderNames.length - 1]}`
: appBuilderNames[0];

return (
<div className={styles.page}>
<p className={styles.lede}>
Expand All @@ -81,6 +97,13 @@ const SkillsPage: React.FC<SkillsPageProps> = ({ framework }) => {
framework={framework}
manualInstallUrl="#manual-installation"
/>
{appBuilders.length > 0 && (
<p className={styles.appBuilders}>
Building in an AI app builder instead? The same skills import
straight from GitHub into {joinNames('and')} —{' '}
<a href="#manual-installation">see the steps</a>.
</p>
)}

<h2>How to use it</h2>
<p>
Expand Down Expand Up @@ -190,8 +213,12 @@ const SkillsPage: React.FC<SkillsPageProps> = ({ framework }) => {

<h2 id="manual-installation">Manual installation</h2>
<p>
The command above covers every agent. If you would rather install from
the plugin marketplace your agent ships with, pick it here.
The command above covers every agent that runs in your project
directory. If you would rather install from the plugin marketplace your
agent ships with{appBuilders.length > 0
? ` — or you build with an AI app builder (${appBuilderNames.join(', ')} and the like) —`
: ','}{' '}
pick it here.
</p>
<ManualInstall framework={frameworkSlug} />

Expand Down
9 changes: 9 additions & 0 deletions src/components/SkillsPage/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,12 @@
.table code {
white-space: nowrap;
}

/* Sits under the one-command install as a secondary route, so it reads a step
below the command block without competing with it. */
.appBuilders {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--ifm-color-content-secondary, #555);
margin: 0.25rem 0 0 0;
}
Loading