Merge: apply an agent profile to a seat (prefill + persona)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,21 @@ interface Agent {
|
|||||||
skillKeys: string[]
|
skillKeys: string[]
|
||||||
mcpServerIds: string[]
|
mcpServerIds: string[]
|
||||||
docs: string[]
|
docs: string[]
|
||||||
|
persona?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentProfileLite {
|
||||||
|
id: string
|
||||||
|
profileKey: string
|
||||||
|
name: string
|
||||||
|
monogram?: string | null
|
||||||
|
recommendedAutonomy: string
|
||||||
|
skillKeys: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AgentProfileDetail {
|
||||||
|
profile: AgentProfileLite
|
||||||
|
body: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTONOMY = [
|
const AUTONOMY = [
|
||||||
@@ -86,6 +101,7 @@ export function SeatsPage() {
|
|||||||
const [mcp, setMcp] = useState({ name: '', endpoint: '', headerName: 'Authorization', headerValue: '' })
|
const [mcp, setMcp] = useState({ name: '', endpoint: '', headerName: 'Authorization', headerValue: '' })
|
||||||
const [newSeat, setNewSeat] = useState('')
|
const [newSeat, setNewSeat] = useState('')
|
||||||
const [selectedSeat, setSelectedSeat] = useState<string | null>(null)
|
const [selectedSeat, setSelectedSeat] = useState<string | null>(null)
|
||||||
|
const [profiles, setProfiles] = useState<AgentProfileLite[]>([])
|
||||||
const [agent, setAgent] = useState({
|
const [agent, setAgent] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
monogram: '',
|
monogram: '',
|
||||||
@@ -94,6 +110,7 @@ export function SeatsPage() {
|
|||||||
skillKeys: [] as string[],
|
skillKeys: [] as string[],
|
||||||
mcpServerIds: [] as string[],
|
mcpServerIds: [] as string[],
|
||||||
docs: '',
|
docs: '',
|
||||||
|
persona: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const run = useCallback(async (action: () => Promise<unknown>) => {
|
const run = useCallback(async (action: () => Promise<unknown>) => {
|
||||||
@@ -128,6 +145,11 @@ export function SeatsPage() {
|
|||||||
const byKey = new Map<string, Skill>()
|
const byKey = new Map<string, Skill>()
|
||||||
for (const s of lib) if (!byKey.has(s.skillKey)) byKey.set(s.skillKey, s)
|
for (const s of lib) if (!byKey.has(s.skillKey)) byKey.set(s.skillKey, s)
|
||||||
setSkills([...byKey.values()])
|
setSkills([...byKey.values()])
|
||||||
|
// Agent profiles (AGENTS.md): one selectable entry per key (the resolvable winner is first).
|
||||||
|
const profileList = await api.get<AgentProfileLite[]>(`/api/orgboard/agent-profiles?organizationId=${organizationId}`)
|
||||||
|
const byProfileKey = new Map<string, AgentProfileLite>()
|
||||||
|
for (const p of profileList) if (!byProfileKey.has(p.profileKey)) byProfileKey.set(p.profileKey, p)
|
||||||
|
setProfiles([...byProfileKey.values()])
|
||||||
await loadConfigs()
|
await loadConfigs()
|
||||||
await loadMcpServers()
|
await loadMcpServers()
|
||||||
})
|
})
|
||||||
@@ -217,11 +239,31 @@ export function SeatsPage() {
|
|||||||
skillKeys: existing.skillKeys,
|
skillKeys: existing.skillKeys,
|
||||||
mcpServerIds: existing.mcpServerIds ?? [],
|
mcpServerIds: existing.mcpServerIds ?? [],
|
||||||
docs: existing.docs.join(', '),
|
docs: existing.docs.join(', '),
|
||||||
|
persona: existing.persona ?? '',
|
||||||
}
|
}
|
||||||
: { name: '', monogram: '', autonomy: 'Gated', apiConfigId: configs[0]?.id ?? '', skillKeys: [], mcpServerIds: [], docs: '' },
|
: { name: '', monogram: '', autonomy: 'Gated', apiConfigId: configs[0]?.id ?? '', skillKeys: [], mcpServerIds: [], docs: '', persona: '' },
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Apply an AGENTS.md profile to the seat: prefill identity, autonomy, skills, and the persona
|
||||||
|
// (operating guide). The user can still tweak everything before saving.
|
||||||
|
const applyProfile = (key: string) =>
|
||||||
|
run(async () => {
|
||||||
|
const versions = await api.get<AgentProfileDetail[]>(`/api/orgboard/agent-profiles/${key}?organizationId=${organizationId}`)
|
||||||
|
const chosen = versions[0]
|
||||||
|
if (!chosen) return
|
||||||
|
const known = new Set(skills.map((s) => s.skillKey))
|
||||||
|
setAgent((a) => ({
|
||||||
|
...a,
|
||||||
|
name: chosen.profile.name,
|
||||||
|
monogram: chosen.profile.monogram ?? '',
|
||||||
|
autonomy: chosen.profile.recommendedAutonomy,
|
||||||
|
skillKeys: chosen.profile.skillKeys.filter((k) => known.has(k)),
|
||||||
|
persona: chosen.body,
|
||||||
|
}))
|
||||||
|
toast.success(`Applied “${chosen.profile.name}”. Review and save.`)
|
||||||
|
})
|
||||||
|
|
||||||
const saveAgent = () =>
|
const saveAgent = () =>
|
||||||
run(async () => {
|
run(async () => {
|
||||||
if (!selectedSeat) return
|
if (!selectedSeat) return
|
||||||
@@ -233,6 +275,7 @@ export function SeatsPage() {
|
|||||||
skillKeys: agent.skillKeys,
|
skillKeys: agent.skillKeys,
|
||||||
mcpServerIds: agent.mcpServerIds,
|
mcpServerIds: agent.mcpServerIds,
|
||||||
docs: agent.docs ? agent.docs.split(',').map((d) => d.trim()).filter(Boolean) : [],
|
docs: agent.docs ? agent.docs.split(',').map((d) => d.trim()).filter(Boolean) : [],
|
||||||
|
persona: agent.persona.trim() || null,
|
||||||
})
|
})
|
||||||
if (teamId) await loadSeats(teamId)
|
if (teamId) await loadSeats(teamId)
|
||||||
toast.success(`${agent.name || 'Agent'} configured — seat is now AI.`)
|
toast.success(`${agent.name || 'Agent'} configured — seat is now AI.`)
|
||||||
@@ -416,6 +459,19 @@ export function SeatsPage() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
{selected && (
|
{selected && (
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{profiles.length > 0 && (
|
||||||
|
<Field label="Start from a profile (AGENTS.md)">
|
||||||
|
<Select value="" onValueChange={applyProfile}>
|
||||||
|
<SelectTrigger className="w-72"><SelectValue placeholder="Apply an agent profile…" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{profiles.map((p) => <SelectItem key={p.profileKey} value={p.profileKey}>{p.name}</SelectItem>)}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap items-end gap-3">
|
<div className="flex flex-wrap items-end gap-3">
|
||||||
<Field label="Name">
|
<Field label="Name">
|
||||||
<Input value={agent.name} onChange={(e) => setAgent({ ...agent, name: e.target.value })} className="w-40" placeholder="Aria" />
|
<Input value={agent.name} onChange={(e) => setAgent({ ...agent, name: e.target.value })} className="w-40" placeholder="Aria" />
|
||||||
@@ -494,6 +550,17 @@ export function SeatsPage() {
|
|||||||
<Input value={agent.docs} onChange={(e) => setAgent({ ...agent, docs: e.target.value })} placeholder="product-docs, house-style" />
|
<Input value={agent.docs} onChange={(e) => setAgent({ ...agent, docs: e.target.value })} placeholder="product-docs, house-style" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>Operating guide (persona)</Label>
|
||||||
|
<textarea
|
||||||
|
value={agent.persona}
|
||||||
|
onChange={(e) => setAgent({ ...agent, persona: e.target.value })}
|
||||||
|
rows={4}
|
||||||
|
placeholder="The agent's persona / operating guide — set by a profile, editable here. Injected into the run."
|
||||||
|
className="w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button onClick={saveAgent} className="self-start">
|
<Button onClick={saveAgent} className="self-start">
|
||||||
<Wand2 data-icon="inline-start" />
|
<Wand2 data-icon="inline-start" />
|
||||||
Save agent
|
Save agent
|
||||||
|
|||||||
Reference in New Issue
Block a user