([]);
+
const openEditModal = useCallback((job: CronJob) => {
setEditJob(job);
setEditPrompt(getJobPrompt(job));
@@ -184,6 +249,7 @@ export default function CronPage() {
);
setEditName(getJobName(job));
setEditDeliver(asText(job.deliver) || "local");
+ setEditSkills(Array.isArray(job.skills) ? job.skills.filter(Boolean) : []);
}, []);
const loadJobs = useCallback(() => {
@@ -217,6 +283,25 @@ export default function CronPage() {
loadJobs();
}, [loadJobs]);
+ // Load installed skills for the profile new jobs will be created under.
+ // "" / "default" maps to the dashboard's own profile via the optional
+ // ?profile= scoping on /api/skills.
+ useEffect(() => {
+ let cancelled = false;
+ api
+ .getSkills(createProfile === "default" ? undefined : createProfile)
+ .then((s) => {
+ if (!cancelled)
+ setAvailableSkills(
+ [...s].sort((a, b) => a.name.localeCompare(b.name)),
+ );
+ })
+ .catch(() => !cancelled && setAvailableSkills([]));
+ return () => {
+ cancelled = true;
+ };
+ }, [createProfile]);
+
const scheduleString = buildScheduleString(scheduleState);
// Label for a delivery option. Configured platforms missing their cron home
@@ -284,6 +369,7 @@ export default function CronPage() {
schedule: scheduleString,
name: name.trim() || undefined,
deliver,
+ skills: jobSkills.length > 0 ? jobSkills : undefined,
},
createProfile,
);
@@ -292,6 +378,7 @@ export default function CronPage() {
setScheduleState(DEFAULT_SCHEDULE_STATE);
setName("");
setDeliver("local");
+ setJobSkills([]);
setCreateModalOpen(false);
loadJobs();
} catch (e) {
@@ -316,6 +403,7 @@ export default function CronPage() {
schedule: editSchedule.trim(),
name: editName.trim(),
deliver: editDeliver,
+ skills: editSkills,
},
getJobProfile(editJob),
);
@@ -524,6 +612,21 @@ export default function CronPage() {
)}
+
+
+
+
+ Selected skills are loaded before the prompt runs — the cron
+ sets when, the skill sets how.
+
+
+
+
+
+
+
+
{hasName && promptText && (
diff --git a/web/src/pages/SkillsPage.tsx b/web/src/pages/SkillsPage.tsx
index 7834de1cf46..e8f764d8e86 100644
--- a/web/src/pages/SkillsPage.tsx
+++ b/web/src/pages/SkillsPage.tsx
@@ -25,6 +25,8 @@ import {
AlertTriangle,
Sparkles,
Loader2,
+ Pencil,
+ Plus,
} from "lucide-react";
import { api } from "@/lib/api";
import type {
@@ -38,6 +40,7 @@ import type {
} from "@/lib/api";
import { useProfileScope } from "@/contexts/useProfileScope";
import { ToolsetConfigDrawer } from "@/components/ToolsetConfigDrawer";
+import { SkillEditorDialog } from "@/components/SkillEditorDialog";
import { useToast } from "@nous-research/ui/hooks/use-toast";
import { Toast } from "@nous-research/ui/ui/components/toast";
import { Card, CardContent, CardHeader, CardTitle } from "@nous-research/ui/ui/components/card";
@@ -130,6 +133,9 @@ export default function SkillsPage() {
const [activeCategory, setActiveCategory] = useState(null);
const [togglingSkills, setTogglingSkills] = useState>(new Set());
const [configToolset, setConfigToolset] = useState(null);
+ // Skill editor dialog: open + which skill is being edited (null = create).
+ const [editorOpen, setEditorOpen] = useState(false);
+ const [editorSkill, setEditorSkill] = useState(null);
const { toast, showToast } = useToast();
const { t } = useI18n();
const { setAfterTitle, setEnd } = usePageHeader();
@@ -201,6 +207,28 @@ export default function SkillsPage() {
}
};
+ /* ---- Skill editor (create / edit SKILL.md) ---- */
+ const openCreateEditor = useCallback(() => {
+ setEditorSkill(null);
+ setEditorOpen(true);
+ }, []);
+ const openEditEditor = useCallback((skillName: string) => {
+ setEditorSkill(skillName);
+ setEditorOpen(true);
+ }, []);
+ const handleEditorSaved = useCallback(
+ (skillName: string) => {
+ showToast(`${skillName} saved ✓`, "success");
+ // Reload the list so a newly created skill (or an edited description)
+ // shows up immediately.
+ api
+ .getSkills(selectedProfile || undefined)
+ .then(setSkills)
+ .catch(() => {});
+ },
+ [selectedProfile, showToast],
+ );
+
/* ---- Derived data ---- */
const lowerSearch = search.toLowerCase();
const isSearching = search.trim().length > 0;
@@ -436,6 +464,7 @@ export default function SkillsPage() {
skill={skill}
toggling={togglingSkills.has(skill.name)}
onToggle={() => handleToggleSkill(skill)}
+ onEdit={() => openEditEditor(skill.name)}
noDescriptionLabel={t.skills.noDescription}
/>
))}
@@ -457,11 +486,22 @@ export default function SkillsPage() {
)
: t.skills.all}
-
- {t.skills.skillCount
- .replace("{count}", String(activeSkills.length))
- .replace("{s}", activeSkills.length !== 1 ? "s" : "")}
-
+
+
+ {t.skills.skillCount
+ .replace("{count}", String(activeSkills.length))
+ .replace("{s}", activeSkills.length !== 1 ? "s" : "")}
+
+ }
+ >
+ New skill
+
+
@@ -479,6 +519,7 @@ export default function SkillsPage() {
skill={skill}
toggling={togglingSkills.has(skill.name)}
onToggle={() => handleToggleSkill(skill)}
+ onEdit={() => openEditEditor(skill.name)}
noDescriptionLabel={t.skills.noDescription}
/>
))}
@@ -583,6 +624,13 @@ export default function SkillsPage() {
onChanged={() => void refreshToolsets()}
/>
)}
+ setEditorOpen(false)}
+ onSaved={handleEditorSaved}
+ />
);
@@ -592,6 +640,7 @@ function SkillRow({
skill,
toggling,
onToggle,
+ onEdit,
noDescriptionLabel,
}: SkillRowProps) {
return (
@@ -617,6 +666,16 @@ function SkillRow({
{skill.description || noDescriptionLabel}
+
);
}
@@ -648,6 +707,7 @@ interface PanelItemProps {
interface SkillRowProps {
noDescriptionLabel: string;
onToggle: () => void;
+ onEdit: () => void;
skill: SkillInfo;
toggling: boolean;
}