This commit is contained in:
@@ -15,6 +15,11 @@ import { SECTION_COMPONENTS } from '@/components/skills/SkillSections';
|
||||
from the module rather than the package index so a page section never pulls
|
||||
the assistant panel in behind it. */
|
||||
import { usePageAction, usePageContext } from '@/components/ai-assistant/PageContext';
|
||||
/* Which agent is answering on this page. Imported from the module rather than
|
||||
the package index for the same reason `PageContext` is: a page section must
|
||||
not pull the assistant panel in behind it. */
|
||||
import { useActiveAgent } from '@/components/ai-assistant/AgentContext';
|
||||
import { agentPermitsSkill } from '@/lib/agents/runtime';
|
||||
|
||||
/**
|
||||
* The extension point: one controlled slot a page offers to skills.
|
||||
@@ -35,23 +40,71 @@ import { usePageAction, usePageContext } from '@/components/ai-assistant/PageCon
|
||||
*/
|
||||
|
||||
/** The skills contributing sections to this page right now. */
|
||||
function useSkillSections(page, placement) {
|
||||
/**
|
||||
* The sections definitions contribute to a page, optionally narrowed to one
|
||||
* placement.
|
||||
*
|
||||
* Exported because the node tree needs the identical reading: a skill's section
|
||||
* drawn as a child node and the same section drawn by this surface must come
|
||||
* from one resolution, or the two would disagree about what a page carries.
|
||||
* Called with no placement it returns every section on the page.
|
||||
*/
|
||||
export function useSkillSections(page, placement) {
|
||||
const preferences = usePreferences();
|
||||
/**
|
||||
* Who is answering here.
|
||||
*
|
||||
* Read through the same context the panel reads, which is mounted around the
|
||||
* page as well as around the panel — see `AssistantPanelContext`. Outside a
|
||||
* provider this returns an inert value, so a surface drawn anywhere else sees
|
||||
* no agents and no ownership, which is exactly today's behaviour.
|
||||
*/
|
||||
const { agent, agents } = useActiveAgent();
|
||||
|
||||
const customKey = JSON.stringify(preferences.customSkills || []);
|
||||
const disabledKey = JSON.stringify(preferences.disabledSkills || []);
|
||||
/* Stable keys, because these are fresh arrays on every render and a memo
|
||||
keyed on their identity would recompute forever. */
|
||||
const ownedKey = JSON.stringify((agents || []).map((a) => a.skills || []));
|
||||
const mineKey = JSON.stringify(agent?.skills || []);
|
||||
|
||||
return useMemo(() => {
|
||||
const custom = JSON.parse(customKey);
|
||||
const disabled = JSON.parse(disabledKey);
|
||||
|
||||
/**
|
||||
* Agent ownership, and why it is opt-in.
|
||||
*
|
||||
* A skill that an agent claims belongs to that agent: its UI is drawn where
|
||||
* that agent is answering and nowhere else. A skill that **no** agent claims
|
||||
* is unowned, and unowned means unchanged — page scope and the account
|
||||
* switch decide it, exactly as they did before this existed.
|
||||
*
|
||||
* That asymmetry is the whole design. Every skill this product ships is
|
||||
* conversation-only and claimed for conversation; the definitions that draw
|
||||
* page UI are authored on the account and claimed by nobody. Enforcing
|
||||
* ownership on all of them would have removed every skill section in the
|
||||
* product on the day it shipped. Attaching a skill to an agent is therefore
|
||||
* the act that brings it under an agent's control — a decision an author
|
||||
* makes in Agent Configure, not one taken on their behalf here.
|
||||
*
|
||||
* Three separate ideas meet here and none of them is the others: what the
|
||||
* *agent* owns, what pages the *skill* declares, and what the *account* has
|
||||
* switched off. All three must pass.
|
||||
*/
|
||||
/* The rule itself lives with the other agent rules, so it can be reasoned
|
||||
about and tested without rendering a page. */
|
||||
const scope = { agents: JSON.parse(ownedKey).map((skills) => ({ skills })), agent: { skills: JSON.parse(mineKey) } };
|
||||
|
||||
return allSkills(custom)
|
||||
/* Inactive means registered but not offered — the same rule the assistant
|
||||
follows, so switching a skill off removes its UI too. */
|
||||
.filter((skill) => skill.status === 'active' && !disabled.includes(skill.id))
|
||||
.filter((skill) => agentPermitsSkill(skill.id, scope))
|
||||
.flatMap((skill) => sectionsForPage(skill, page)
|
||||
.filter((section) => !placement || section.placement === placement)
|
||||
.map((section) => ({ skill, section })));
|
||||
}, [page, placement, customKey, disabledKey]);
|
||||
}, [page, placement, customKey, disabledKey, ownedKey, mineKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user