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
39 changes: 21 additions & 18 deletions apps/web/components/dashboard/release-skill-metadata-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ function SkillEditorRow({
const [runtimeEnforcement, setRuntimeEnforcement] = useState(skill.enforcement ?? "advisory");
const [runtimePriority, setRuntimePriority] = useState(skill.priority ?? 50);
const [runtimeVersion, setRuntimeVersion] = useState(skill.version ?? "0.0.0");
const [runtimeDirty, setRuntimeDirty] = useState(false);
/** Errors returned from the API after a failed save (merged with live validation for display). */
const [serverIssues, setServerIssues] = useState<McpMetadataIssue[]>([]);
/** When false and body is valid non-empty, show rendered markdown; click or focus opens the editor. */
Expand Down Expand Up @@ -236,6 +237,7 @@ function SkillEditorRow({
setRuntimeEnforcement(skill.enforcement ?? "advisory");
setRuntimePriority(skill.priority ?? 50);
setRuntimeVersion(skill.version ?? "0.0.0");
setRuntimeDirty(false);
setServerIssues([]);
setSkillBodyEditing(false);
}, [skill]);
Expand All @@ -260,21 +262,20 @@ function SkillEditorRow({
failure_modes: listFromMultiline(failureModesText),
invoke_first: invokeFirst,
},
runtime: {
};
if (runtimeDirty) {
payload.runtime = {
kind: runtimeKind,
scope: runtimeScope,
activation: {
mode: runtimeActivation,
intents: listFromMultiline(useWhenText),
events: [],
tags: [],
examples: [],
},
enforcement: runtimeEnforcement,
priority: runtimePriority,
version: runtimeVersion.trim() || "0.0.0",
},
};
};
}
if (schemaDirty) {
payload.replace_schema = true;
payload.schema_json = schemaJson;
Expand All @@ -284,6 +285,7 @@ function SkillEditorRow({
onSuccess: () => {
setServerIssues([]);
setSchemaDirty(false);
setRuntimeDirty(false);
queryClient.invalidateQueries({
queryKey: ["compiled-skills", projectId, releaseId],
});
Expand Down Expand Up @@ -445,9 +447,9 @@ function SkillEditorRow({
size="sm"
onClick={() => writebackMutation.mutate()}
disabled={writebackMutation.isPending || skill.clarification_required === true}
title={skill.clarification_required ? "Resolve and save runtime clarification before write-back" : "Open a draft GitHub pull request"}
title={skill.clarification_required ? "Resolve and save runtime clarification before opening a sidecar pull request" : "Open a draft pull request for .mycontext/skills.yaml"}
>
{writebackMutation.isPending ? "Opening PR…" : "Write Back"}
{writebackMutation.isPending ? "Opening PR…" : "Open Sidecar PR"}
</Button>
<Button
type="button"
Expand All @@ -464,7 +466,7 @@ function SkillEditorRow({
return (
<div className="space-y-3 rounded-lg border p-4">
{headerBar}
{writebackMutation.error ? <p className="text-destructive text-xs">Could not open the metadata pull request. Confirm repository write and pull-request permissions.</p> : null}
{writebackMutation.error ? <p className="text-destructive text-xs">Could not open the sidecar policy pull request. Confirm repository write and pull-request permissions.</p> : null}
{reviewFieldSet.size > 0 ? (
<p
className="rounded-md border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs leading-snug text-amber-950 dark:text-amber-100"
Expand Down Expand Up @@ -642,46 +644,46 @@ function SkillEditorRow({
</p>
{skill.clarification_required ? (
<p className="mt-2 text-xs font-medium text-amber-800 dark:text-amber-200" role="status">
Clarification required: {(skill.clarification_questions ?? []).map((question) => question.field).join(", ") || "runtime metadata"}.
Clarification required: {(skill.clarification_questions ?? []).map((question) => question.question).join(" ") || "Review the runtime policy before publishing."}
</p>
) : null}
</div>
<div className="grid gap-3 md:grid-cols-3">
<div className="space-y-1.5">
<Label className="text-xs">Kind</Label>
<Select value={runtimeKind} onValueChange={(value) => setRuntimeKind(value as typeof runtimeKind)}>
<Select value={runtimeKind} onValueChange={(value) => { setRuntimeKind(value as typeof runtimeKind); setRuntimeDirty(true); }}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{["operating", "task", "tool-use", "reference"].map((value) => <SelectItem key={value} value={value}>{selectOptionTitleCase(value)}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Scope</Label>
<Select value={runtimeScope} onValueChange={(value) => setRuntimeScope(value as typeof runtimeScope)}>
<Select value={runtimeScope} onValueChange={(value) => { setRuntimeScope(value as typeof runtimeScope); setRuntimeDirty(true); }}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{["global", "organization", "workspace", "repository", "task"].map((value) => <SelectItem key={value} value={value}>{selectOptionTitleCase(value)}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Activation</Label>
<Select value={runtimeActivation} onValueChange={(value) => setRuntimeActivation(value as typeof runtimeActivation)}>
<Select value={runtimeActivation} onValueChange={(value) => { setRuntimeActivation(value as typeof runtimeActivation); setRuntimeDirty(true); }}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{["always", "intent", "event", "explicit"].map((value) => <SelectItem key={value} value={value}>{selectOptionTitleCase(value)}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Enforcement</Label>
<Select value={runtimeEnforcement} onValueChange={(value) => setRuntimeEnforcement(value as typeof runtimeEnforcement)}>
<Select value={runtimeEnforcement} onValueChange={(value) => { setRuntimeEnforcement(value as typeof runtimeEnforcement); setRuntimeDirty(true); }}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>{["advisory", "required"].map((value) => <SelectItem key={value} value={value}>{selectOptionTitleCase(value)}</SelectItem>)}</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor={`runtime-priority-${skill.id}`} className="text-xs">Priority</Label>
<Input id={`runtime-priority-${skill.id}`} type="number" min={0} max={100} value={runtimePriority} onChange={(event) => setRuntimePriority(Math.min(100, Math.max(0, Number(event.target.value))))} />
<Input id={`runtime-priority-${skill.id}`} type="number" min={0} max={100} value={runtimePriority} onChange={(event) => { setRuntimePriority(Math.min(100, Math.max(0, Number(event.target.value)))); setRuntimeDirty(true); }} />
</div>
<div className="space-y-1.5">
<Label htmlFor={`runtime-version-${skill.id}`} className="text-xs">Version</Label>
<Input id={`runtime-version-${skill.id}`} value={runtimeVersion} onChange={(event) => setRuntimeVersion(event.target.value)} placeholder="1.0.0" />
<Input id={`runtime-version-${skill.id}`} value={runtimeVersion} onChange={(event) => { setRuntimeVersion(event.target.value); setRuntimeDirty(true); }} placeholder="source revision + checksum" />
</div>
</div>
</div>
Expand All @@ -702,9 +704,10 @@ function SkillEditorRow({
)}
>
<div>
<p className="text-xs font-medium">SKILL Routing (Front Matter)</p>
<p className="text-xs font-medium">Skill Routing (Source + Sidecar)</p>
<p className="text-muted-foreground mt-0.5 text-xs leading-snug">
One phrase per line (same as comma-separated lists in SKILL.md). Shown in MCP{" "}
One phrase per line. Source routing is preserved; deployment policy is written to{" "}
<code className="font-mono text-[0.7rem]">.mycontext/skills.yaml</code>. Hints are shown in MCP{" "}
<code className="font-mono text-[0.7rem]">resources/list</code> and at the top of{" "}
<code className="font-mono text-[0.7rem]">resources/read</code> when exposure is{" "}
<span className="font-medium">resource</span>. Values are stored in the release even
Expand Down
55 changes: 20 additions & 35 deletions apps/web/components/dashboard/skill-runtime-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ import { fetchProjectSkillRuntime, updateProjectSkillRuntime } from "@/lib/proje
import type { SkillRuntimeAssignment } from "@/lib/types";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";

const DEFAULT_ASSIGNMENT: SkillRuntimeAssignment = {
function defaultAssignment(): SkillRuntimeAssignment {
return {
skill_id: "",
scope: "workspace",
activation_mode: "always",
target_type: "workspace",
target_id: "",
required: false,
priority: 50,
};
};
}

export function SkillRuntimeSection({ projectId }: { projectId: string }) {
const queryClient = useQueryClient();
Expand All @@ -25,23 +28,15 @@ export function SkillRuntimeSection({ projectId }: { projectId: string }) {
queryFn: () => fetchProjectSkillRuntime(projectId),
});
const [telemetryEnabled, setTelemetryEnabled] = useState(false);
const [semanticEnabled, setSemanticEnabled] = useState(false);
const [feedbackEnabled, setFeedbackEnabled] = useState(false);
const [provider, setProvider] = useState("");
const [model, setModel] = useState("");
const [preferences, setPreferences] = useState("");
const [assignments, setAssignments] = useState<SkillRuntimeAssignment[]>([]);
const [draft, setDraft] = useState<SkillRuntimeAssignment>(DEFAULT_ASSIGNMENT);
const [draft, setDraft] = useState<SkillRuntimeAssignment>(() => defaultAssignment());

/* eslint-disable react-hooks/set-state-in-effect -- reset editable runtime settings from the fetched server snapshot */
useEffect(() => {
if (!query.data) return;
setTelemetryEnabled(query.data.telemetry_enabled);
setSemanticEnabled(query.data.semantic_enabled);
setFeedbackEnabled(query.data.feedback_issue_creation_enabled);
setProvider(query.data.embedding_provider ?? "");
setModel(query.data.embedding_model ?? "");
setPreferences(query.data.provider_preferences_json ?? "");
setAssignments(query.data.assignments);
}, [query.data]);
/* eslint-enable react-hooks/set-state-in-effect */
Expand All @@ -50,11 +45,8 @@ export function SkillRuntimeSection({ projectId }: { projectId: string }) {
mutationFn: () => updateProjectSkillRuntime(projectId, {
telemetry_enabled: telemetryEnabled,
telemetry_retention_days: 30,
semantic_enabled: semanticEnabled,
embedding_provider: provider || null,
embedding_model: model || null,
semantic_enabled: false,
feedback_issue_creation_enabled: feedbackEnabled,
provider_preferences_json: preferences,
assignments,
}),
onSuccess: (value) => queryClient.setQueryData(["skill-runtime", projectId], value),
Expand All @@ -68,39 +60,32 @@ export function SkillRuntimeSection({ projectId }: { projectId: string }) {
<div>
<h3 className="font-medium">Portable Skill Runtime</h3>
<p className="text-muted-foreground mt-1 text-sm">
Configure deterministic activation, optional semantic ranking, feedback authorization, and privacy-conscious traces.
Configure deterministic activation, feedback draft authorization, and privacy-conscious traces.
</p>
</div>

<div className="grid gap-4 md:grid-cols-3">
<RuntimeCheckbox label="Semantic Ranking" description="Uses the configured provider only within deterministic precedence bands." checked={semanticEnabled} onChange={setSemanticEnabled} />
<RuntimeCheckbox label="30-Day Telemetry" description="Opt-in hashed resolution events; full prompts and code are not stored." checked={telemetryEnabled} onChange={setTelemetryEnabled} />
<RuntimeCheckbox label="Feedback Issue Creation" description="Allows explicit feedback calls to request an external issue action." checked={feedbackEnabled} onChange={setFeedbackEnabled} />
</div>

<div className="grid gap-3 md:grid-cols-2">
<div className="space-y-1.5"><Label htmlFor="embedding-provider">Embedding Provider</Label><Input id="embedding-provider" value={provider} onChange={(event) => setProvider(event.target.value)} placeholder="openai-compatible" /></div>
<div className="space-y-1.5"><Label htmlFor="embedding-model">Embedding Model</Label><Input id="embedding-model" value={model} onChange={(event) => setModel(event.target.value)} placeholder="text-embedding-3-small" /></div>
</div>
<div className="space-y-1.5">
<Label htmlFor="provider-preferences">Provider Preferences JSON</Label>
<textarea id="provider-preferences" value={preferences} onChange={(event) => setPreferences(event.target.value)} rows={3} placeholder='{"issue.create":"linear"}' className="w-full rounded-md border bg-transparent px-3 py-2 font-mono text-xs" />
<div className="grid gap-4 md:grid-cols-2">
<RuntimeCheckbox label="30-Day Telemetry" description="Opt-in redacted resolution events; prompts, skill bodies, code, and credentials are not stored." checked={telemetryEnabled} onChange={setTelemetryEnabled} />
<RuntimeCheckbox label="Feedback Issue Drafts" description="Allows explicit feedback calls to prepare an external issue draft; it does not claim the issue was created." checked={feedbackEnabled} onChange={setFeedbackEnabled} />
</div>

<div className="space-y-3 border-t pt-4">
<div><h4 className="text-sm font-medium">Scoped Assignments</h4><p className="text-muted-foreground text-xs">Explicit assignments take precedence over inferred matches.</p></div>
{assignments.map((assignment, index) => (
<div key={`${assignment.skill_id}-${assignment.scope}-${index}`} className="flex flex-wrap items-center gap-2 rounded-md border px-3 py-2 text-sm">
<code className="mr-auto">{assignment.skill_id}</code><span>{assignment.scope}</span><span>{assignment.activation_mode}</span><span>Priority {assignment.priority}</span>
<div key={`${assignment.skill_id}-${assignment.scope}-${assignment.target_type}-${assignment.target_id}-${index}`} className="flex flex-wrap items-center gap-2 rounded-md border px-3 py-2 text-sm">
<code className="mr-auto">{assignment.skill_id}</code><span>{assignment.scope}</span><span>{assignment.activation_mode}</span><span>{assignment.target_type}: {assignment.target_id}</span><span>{assignment.required ? "Required" : "Advisory"}</span><span>Priority {assignment.priority}</span>
<Button type="button" size="sm" variant="ghost" onClick={() => setAssignments((rows) => rows.filter((_, rowIndex) => rowIndex !== index))}>Remove</Button>
</div>
))}
<div className="grid gap-2 md:grid-cols-[1fr_10rem_10rem_7rem_auto]">
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-[1fr_9rem_9rem_9rem_1fr_7rem_auto_auto]">
<Input aria-label="Skill ID" value={draft.skill_id} onChange={(event) => setDraft((value) => ({ ...value, skill_id: event.target.value }))} placeholder="skill-id" />
<Select value={draft.scope} onValueChange={(scope) => setDraft((value) => ({ ...value, scope: scope as SkillRuntimeAssignment["scope"] }))}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent>{["global", "organization", "workspace", "repository", "task"].map((scope) => <SelectItem key={scope} value={scope}>{scope}</SelectItem>)}</SelectContent></Select>
<Select value={draft.scope} onValueChange={(scope) => setDraft((value) => ({ ...value, scope: scope as SkillRuntimeAssignment["scope"], target_type: scope as SkillRuntimeAssignment["target_type"], target_id: scope === "global" ? "*" : "" }))}><SelectTrigger aria-label="Assignment scope"><SelectValue /></SelectTrigger><SelectContent>{["global", "organization", "workspace", "repository", "task"].map((scope) => <SelectItem key={scope} value={scope}>{scope}</SelectItem>)}</SelectContent></Select>
<Select value={draft.activation_mode} onValueChange={(activation_mode) => setDraft((value) => ({ ...value, activation_mode: activation_mode as SkillRuntimeAssignment["activation_mode"] }))}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent>{["always", "intent", "event", "explicit"].map((mode) => <SelectItem key={mode} value={mode}>{mode}</SelectItem>)}</SelectContent></Select>
<span className="flex items-center rounded-md border px-3 text-sm" aria-label="Assignment target type">{draft.target_type}</span>
<Input aria-label="Assignment target ID" value={draft.target_id} onChange={(event) => setDraft((value) => ({ ...value, target_id: event.target.value }))} placeholder="Exact target identity" />
<Input aria-label="Priority" type="number" min={0} max={100} value={draft.priority} onChange={(event) => setDraft((value) => ({ ...value, priority: Number(event.target.value) }))} />
<Button type="button" variant="outline" disabled={!draft.skill_id.trim()} onClick={() => { setAssignments((rows) => [...rows, { ...draft, skill_id: draft.skill_id.trim() }]); setDraft(DEFAULT_ASSIGNMENT); }}>Add</Button>
<label className="flex items-center gap-2 px-2 text-sm"><input type="checkbox" checked={draft.required} onChange={(event) => setDraft((value) => ({ ...value, required: event.target.checked }))} />Required</label>
<Button type="button" variant="outline" disabled={!draft.skill_id.trim() || !draft.target_id.trim()} onClick={() => { setAssignments((rows) => [...rows, { ...draft, skill_id: draft.skill_id.trim(), target_id: draft.target_id.trim() }]); setDraft(defaultAssignment()); }}>Add</Button>
</div>
</div>

Expand Down
8 changes: 7 additions & 1 deletion apps/web/lib/projects-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,13 @@ export async function updateCompiledSkill(
runtime?: {
kind?: "operating" | "task" | "tool-use" | "reference";
scope?: "global" | "organization" | "workspace" | "repository" | "task";
activation?: { mode: "always" | "intent" | "event" | "explicit"; intents: string[]; events: string[]; tags: string[]; examples: string[] };
activation?: {
mode?: "always" | "intent" | "event" | "explicit";
intents?: string[];
events?: string[];
tags?: string[];
examples?: string[];
};
enforcement?: "advisory" | "required";
priority?: number;
version?: string;
Expand Down
Loading
Loading