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
10 changes: 10 additions & 0 deletions dashboard/src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ export const cloneDefaultSettings = (): DashboardSettings => ({
agentPresetIds: [...DEFAULT_DASHBOARD_SETTINGS.agents.qualityAssurance.completedTaskWithoutPr.agentPresetIds],
},
},
selfReflection: {
planning: {
...DEFAULT_DASHBOARD_SETTINGS.agents.selfReflection.planning,
criteria: DEFAULT_DASHBOARD_SETTINGS.agents.selfReflection.planning.criteria.map((criterion) => ({ ...criterion })),
},
qualityAssurance: {
...DEFAULT_DASHBOARD_SETTINGS.agents.selfReflection.qualityAssurance,
criteria: DEFAULT_DASHBOARD_SETTINGS.agents.selfReflection.qualityAssurance.criteria.map((criterion) => ({ ...criterion })),
},
},
},
skills: DEFAULT_DASHBOARD_SETTINGS.skills.map((skill) => ({ ...skill })),
mcpTools: DEFAULT_DASHBOARD_SETTINGS.mcpTools.map((tool) => ({ ...tool })),
Expand Down
12 changes: 12 additions & 0 deletions dashboard/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ import type {
AutomationInterventionsSettings,
ProviderSettings,
SkillToggle,
SkillStorageKind,
SkillSourceType,
SkillStorageRecord,
SkillRecord,
SkillEmbeddingMetadata,
AgentSkillStorageAttachment,
McpToolToggle,
CustomMcpServer,
CustomMcpTransport,
Expand Down Expand Up @@ -137,6 +143,12 @@ export type {
AutomationInterventionsSettings,
ProviderSettings,
SkillToggle,
SkillStorageKind,
SkillSourceType,
SkillStorageRecord,
SkillRecord,
SkillEmbeddingMetadata,
AgentSkillStorageAttachment,
McpToolToggle,
CustomMcpServer,
CustomMcpTransport,
Expand Down
19 changes: 18 additions & 1 deletion dashboard/src/v2/AgentsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import type { FunctionComponent } from "preact";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import gsap from "gsap";
import { Bot, Plus, Info, ShieldCheck, AlertTriangle, Database, FileText, CheckCircle2, GitBranch, Loader2, ExternalLink } from "lucide-preact";
import type { AgentPreset } from "./types.js";
import type { AgentPreset, SkillStorageRecord } from "./types.js";
import type { InstructionFileSummary, InstructionFileContent } from "./lib/instruction-file-api.js";
import { fetchInstructionFiles } from "./lib/instruction-file-api.js";
import { useProjectData } from "./context/project-data.js";
import {
createAgentPreset,
deleteAgentPreset,
fetchAgentPresets,
fetchSkillStorages,
importAgentPresetFromMarkdown,
pushAgentPresetsToRepository,
syncAllAgentPresetsFromMarkdown,
Expand Down Expand Up @@ -114,6 +115,7 @@ export const AgentsPage: FunctionComponent = () => {
const [selectedAgentUsageLoading, setSelectedAgentUsageLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [instructionFiles, setInstructionFiles] = useState<InstructionFileSummary[]>([]);
const [skillStorages, setSkillStorages] = useState<SkillStorageRecord[]>([]);
const [selectedFileId, setSelectedFileId] = useState<string | null>(null);
const {
data: effectiveSettings,
Expand Down Expand Up @@ -171,10 +173,23 @@ export const AgentsPage: FunctionComponent = () => {
}
};

const refreshSkillStorages = async (): Promise<void> => {
if (!selectedProject) {
setSkillStorages([]);
return;
}
try {
setSkillStorages(await fetchSkillStorages(selectedProject.id));
} catch {
setSkillStorages([]);
}
};

useEffect(() => {
setSelectedFileId(null);
void refreshPresets();
void refreshInstructionFiles();
void refreshSkillStorages();
}, [selectedProject?.id]);

const handleInstructionFileSaved = (updated: InstructionFileContent): void => {
Expand Down Expand Up @@ -858,6 +873,7 @@ export const AgentsPage: FunctionComponent = () => {
defaultMemoryInstruction={effectiveSettings?.settings.memory.workerLearningsInstruction || ""}
providerOptions={providerOptions}
availableMcpServers={availableMcpServers}
availableSkillStorages={skillStorages}
onSave={handleSave}
onCancel={() => setIsEditing(false)}
/>
Expand All @@ -867,6 +883,7 @@ export const AgentsPage: FunctionComponent = () => {
routeTags={routeTagsByPresetId.get(selectedPreset.id) ?? []}
providerOptions={providerOptions}
availableMcpServers={availableMcpServers}
availableSkillStorages={skillStorages}
usageSummary={selectedAgentUsage}
usageLoading={selectedAgentUsageLoading}
onEdit={() => setIsEditing(true)}
Expand Down
38 changes: 37 additions & 1 deletion dashboard/src/v2/components/agents/AgentPresetDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
fetchKnowledgeDocuments,
type KnowledgeDocument,
} from "../../lib/knowledge-api.js";
import type { AgentPreset, CustomMcpServer } from "../../types.js";
import type { AgentPreset, CustomMcpServer, SkillStorageRecord } from "../../types.js";
import type { AgentProviderOption } from "./AgentPresetEditorPanel.js";
import type { AgentAvatarExpression } from "../../lib/agent-avatar.js";
import { AgentAvatarStage } from "./AgentAvatarStage.js";
Expand Down Expand Up @@ -182,6 +182,7 @@ export const AgentPresetDetailPanel: FunctionComponent<{
routeTags: string[];
providerOptions?: AgentProviderOption[];
availableMcpServers?: CustomMcpServer[];
availableSkillStorages?: SkillStorageRecord[];
usageSummary?: AgentUsageSummary | null;
usageLoading?: boolean;
onEdit: () => void;
Expand All @@ -194,6 +195,7 @@ export const AgentPresetDetailPanel: FunctionComponent<{
routeTags,
providerOptions = [],
availableMcpServers = [],
availableSkillStorages = [],
usageSummary,
usageLoading = false,
onEdit,
Expand All @@ -214,6 +216,10 @@ export const AgentPresetDetailPanel: FunctionComponent<{
const mcpTags = resolveAgentMcpTags(preset.mcpAccess, availableMcpServers);
const visibleMcpTags = mcpTags.slice(0, 6);
const hiddenMcpTagCount = mcpTags.length - visibleMcpTags.length;
const attachedSkillStorages = (preset.persistentSkillStorageIds ?? [])
.map((storageId) => availableSkillStorages.find((storage) => storage.id === storageId) ?? null)
.filter((storage): storage is SkillStorageRecord => Boolean(storage));
const persistentSkillsActive = Boolean(preset.persistentSkillStorage?.enabled && attachedSkillStorages.length > 0);

useLayoutEffect(() => {
if (!panelRef.current) return;
Expand Down Expand Up @@ -397,6 +403,36 @@ export const AgentPresetDetailPanel: FunctionComponent<{
{/* Knowledge subscriptions */}
<AgentKnowledgeSummary preset={preset} />

{/* Persistent skills */}
<div className="flex flex-col gap-3">
<SectionHeader icon={Library} title="Persistent Skills" />
<div className="rounded-2xl border border-black/[0.05] bg-white/40 p-4 backdrop-blur-md dark:border-white/[0.05] dark:bg-white/[0.02]">
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="text-xs leading-relaxed text-slate-500 dark:text-slate-400">
Persistent skill retrieval is separate from memory and knowledge documents.
</div>
<span className={`w-fit rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-[0.14em] ${persistentSkillsActive ? "border-signal-500/25 bg-signal-500/[0.08] text-signal-700 dark:text-signal-200" : "border-black/[0.06] bg-black/[0.03] text-slate-500 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-slate-400"}`}>
{persistentSkillsActive ? "Enabled" : "Default off"}
</span>
</div>
<div className="mt-3 flex flex-wrap gap-2">
{attachedSkillStorages.length === 0 ? (
<span className="inline-flex rounded-full border border-black/[0.06] bg-white/50 px-2.5 py-1 text-[11px] font-bold text-slate-400 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-slate-500">
No storage attached
</span>
) : attachedSkillStorages.map((storage) => (
<span
key={storage.id}
className="inline-flex items-center gap-1.5 rounded-full border border-signal-500/20 bg-signal-500/[0.08] px-2.5 py-1 text-[11px] font-bold text-signal-700 dark:text-signal-200"
>
<Library className="h-3 w-3" strokeWidth={2.4} />
{storage.name}
</span>
))}
</div>
</div>
</div>

{/* System Instructions */}
<div className="flex flex-col gap-2">
<SectionHeader
Expand Down
81 changes: 79 additions & 2 deletions dashboard/src/v2/components/agents/AgentPresetEditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ import {
Palette,
SlidersHorizontal,
Library,
Database,
} from "lucide-preact";
import type { AgentMcpAccessConfig, AgentPreset, CustomMcpServer } from "../../types.js";
import type { AgentMcpAccessConfig, AgentPreset, CustomMcpServer, SkillStorageRecord } from "../../types.js";
import type { AgentAvatarExpression } from "../../lib/agent-avatar.js";
import { DEFAULT_AGENT_MEMORY_CONFIG, type AgentMemoryConfig } from "../../memory-types.js";
import { AgentMemoryConfigPanel } from "./AgentMemoryConfigPanel.js";
Expand Down Expand Up @@ -216,9 +217,10 @@ export const AgentPresetEditorPanel: FunctionComponent<{
defaultMemoryInstruction?: string;
providerOptions?: AgentProviderOption[];
availableMcpServers?: CustomMcpServer[];
availableSkillStorages?: SkillStorageRecord[];
onSave: (id: string, updates: Partial<AgentPreset>) => void;
onCancel: () => void;
}> = ({ preset, saving, defaultMemoryInstruction = "", providerOptions = [], availableMcpServers = [], onSave, onCancel }) => {
}> = ({ preset, saving, defaultMemoryInstruction = "", providerOptions = [], availableMcpServers = [], availableSkillStorages = [], onSave, onCancel }) => {
const panelRef = useRef<HTMLFormElement>(null);
const nameRef = useRef<HTMLInputElement>(null);
const descriptionRef = useRef<HTMLTextAreaElement>(null);
Expand All @@ -242,6 +244,8 @@ export const AgentPresetEditorPanel: FunctionComponent<{
const [memoryConfig, setMemoryConfig] = useState<AgentMemoryConfig>(
preset.memoryConfig ?? DEFAULT_AGENT_MEMORY_CONFIG
);
const [persistentSkillStorageIds, setPersistentSkillStorageIds] = useState<string[]>(preset.persistentSkillStorageIds ?? []);
const [persistentSkillsEnabled, setPersistentSkillsEnabled] = useState(Boolean(preset.persistentSkillStorage?.enabled));
const [showMemoryPanel, setShowMemoryPanel] = useState(false);
const memoryButtonRef = useRef<HTMLButtonElement>(null);
const [touched, setTouched] = useState<Record<string, boolean>>({});
Expand Down Expand Up @@ -275,6 +279,8 @@ export const AgentPresetEditorPanel: FunctionComponent<{
setAvatarConfig(preset.avatarConfig);
setMcpAccess(normalizeAgentMcpAccess(preset.mcpAccess ?? defaultAgentMcpAccess()));
setMemoryConfig(preset.memoryConfig ?? DEFAULT_AGENT_MEMORY_CONFIG);
setPersistentSkillStorageIds(preset.persistentSkillStorageIds ?? []);
setPersistentSkillsEnabled(Boolean(preset.persistentSkillStorage?.enabled));
setShowMemoryPanel(false);
setTouched({});
setKnowledgeDirty(false);
Expand Down Expand Up @@ -314,6 +320,8 @@ export const AgentPresetEditorPanel: FunctionComponent<{
if (JSON.stringify(avatarConfig ?? {}) !== JSON.stringify(preset.avatarConfig ?? {})) return true;
if (JSON.stringify(mcpAccess) !== JSON.stringify(normalizeAgentMcpAccess(preset.mcpAccess ?? defaultAgentMcpAccess()))) return true;
if (JSON.stringify(memoryConfig) !== JSON.stringify(preset.memoryConfig ?? DEFAULT_AGENT_MEMORY_CONFIG)) return true;
if (JSON.stringify(persistentSkillStorageIds) !== JSON.stringify(preset.persistentSkillStorageIds ?? [])) return true;
if (persistentSkillsEnabled !== Boolean(preset.persistentSkillStorage?.enabled)) return true;
return false;
}, [
name,
Expand All @@ -326,6 +334,8 @@ export const AgentPresetEditorPanel: FunctionComponent<{
avatarConfig,
mcpAccess,
memoryConfig,
persistentSkillStorageIds,
persistentSkillsEnabled,
preset,
knowledgeDirty,
]);
Expand Down Expand Up @@ -375,6 +385,8 @@ export const AgentPresetEditorPanel: FunctionComponent<{
avatarConfig,
mcpAccess,
memoryConfig,
persistentSkillStorageIds,
persistentSkillStorage: { enabled: persistentSkillsEnabled && persistentSkillStorageIds.length > 0 },
});
setKnowledgeDirty(false);
};
Expand Down Expand Up @@ -436,6 +448,7 @@ export const AgentPresetEditorPanel: FunctionComponent<{
const visibleMcpItems = mcpItems.slice(0, 5);
const hiddenMcpCount = mcpItems.length - visibleMcpItems.length;
const activeMcpCount = mcpItems.filter((item) => item.active).length;
const persistentSkillsActive = persistentSkillsEnabled && persistentSkillStorageIds.length > 0;

const toggleMcpItem = (item: (typeof mcpItems)[number]): void => {
setActionStatus({
Expand Down Expand Up @@ -824,6 +837,70 @@ export const AgentPresetEditorPanel: FunctionComponent<{
</div>
</SectionCard>

<SectionCard icon={Database} eyebrow="Persistent Skills" title="Storage Attachments">
<div className="flex flex-col gap-4 rounded-2xl border border-black/[0.05] bg-white/30 p-5 backdrop-blur-md dark:border-white/[0.05] dark:bg-white/[0.02]">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="text-sm font-bold text-slate-800 dark:text-slate-100">
Persistent skill retrieval
</div>
<p className="mt-1 text-[12px] leading-relaxed text-slate-500 dark:text-slate-400">
Attach durable skill storages to this agent. Retrieval is disabled until storage is attached and this opt-in is enabled.
</p>
</div>
<div className="flex shrink-0 items-center gap-3">
<span className={`rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-[0.14em] ${persistentSkillsActive ? "border-signal-500/25 bg-signal-500/[0.08] text-signal-700 dark:text-signal-200" : "border-black/[0.06] bg-black/[0.03] text-slate-500 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-slate-400"}`}>
{persistentSkillsActive ? "Enabled" : "Default off"}
</span>
<label className="relative inline-flex cursor-pointer shrink-0 items-center">
<input
type="checkbox"
aria-label="Enable persistent skill retrieval"
checked={persistentSkillsEnabled}
disabled={saving || persistentSkillStorageIds.length === 0}
onChange={(event) => setPersistentSkillsEnabled(event.currentTarget.checked)}
className="peer sr-only"
/>
<div className="h-6 w-11 rounded-full border border-black/[0.08] bg-slate-200 transition-colors peer-checked:border-signal-500/40 peer-checked:bg-signal-500/30 peer-focus-visible:ring-2 peer-focus-visible:ring-signal-500/30 peer-disabled:opacity-50 dark:border-white/[0.08] dark:bg-void-800" />
<div className="absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow-sm transition-all peer-checked:translate-x-5 peer-checked:bg-signal-500 dark:bg-slate-500 dark:peer-checked:bg-signal-400" />
</label>
</div>
</div>
<div className="flex flex-wrap gap-2">
{availableSkillStorages.length === 0 ? (
<div className="rounded-[1rem] border border-dashed border-black/[0.06] bg-black/[0.02] px-4 py-3 text-xs leading-relaxed text-slate-500 dark:border-white/[0.06] dark:bg-white/[0.02] dark:text-slate-400">
No project skill storages are available. Create one in Settings, Agents.
</div>
) : availableSkillStorages.map((storage) => {
const checked = persistentSkillStorageIds.includes(storage.id);
return (
<label
key={storage.id}
className={`inline-flex cursor-pointer items-center gap-2 rounded-full border px-3 py-2 text-[11px] font-semibold transition-colors ${checked ? "border-signal-500/30 bg-signal-500/[0.1] text-signal-800 dark:text-signal-100" : "border-black/[0.06] bg-black/[0.02] text-slate-600 dark:border-white/[0.06] dark:bg-white/[0.03] dark:text-slate-300"}`}
>
<input
type="checkbox"
checked={checked}
disabled={saving}
onChange={() => {
const nextIds = checked
? persistentSkillStorageIds.filter((id) => id !== storage.id)
: [...persistentSkillStorageIds, storage.id];
setPersistentSkillStorageIds(nextIds);
if (nextIds.length === 0) {
setPersistentSkillsEnabled(false);
}
}}
className="h-4 w-4 rounded border-black/20 text-signal-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-focus-ring)]"
/>
{storage.name}
</label>
);
})}
</div>
</div>
</SectionCard>

{/* Knowledge subscriptions */}
<SectionCard icon={Library} eyebrow="Grounding" title="Knowledge Base">
<p className="-mt-1 text-[12px] leading-relaxed text-slate-500 dark:text-slate-400">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const CATEGORIES: Category[] = [
{ id: "models", num: "03", label: "AI Models", icon: Cpu, description: "Provider routing, models, and weighting" },
{ id: "sprint", num: "04", label: "Sprint & Git", icon: Target, description: "Git flow, branch naming, merge rules, and execution runtime" },
{ id: "browser", num: "05", label: "Browser Preview", icon: Compass, description: "Preview runtime, browser visibility, and container policy" },
{ id: "agents", num: "06", label: "Agents", icon: Bot, description: "Project-local markdown mirrors and agent authoring behavior" },
{ id: "agents", num: "06", label: "Agents", icon: Bot, description: "Agent routing, skill storage, reflection, and authoring behavior" },
{ id: "memory", num: "07", label: "Memory", icon: BrainCircuit, description: "Embedding models, auto-capture, and promotion policy" },
{ id: "integrations", num: "08", label: "Integrations", icon: Plug, description: "Provider keys, Git hosts, and external connection policy" },
{ id: "mcp", num: "09", label: "MCP", icon: Server, description: "MCP servers injected into CLIs and built-in tool access" },
Expand Down
Loading