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
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {useAtom, useAtomValue, useStore} from "jotai"
import {useOptionalDrillIn} from "../components/MoleculeDrillInContext"

import {AddTextLink} from "./AddTextLink"
import {useAutoExpandOnPopulate} from "./agentSectionAutoExpand"
import {AgentIntegrationDrawer} from "./agentTemplate/AgentIntegrationDrawer"
import {countSummary} from "./agentTemplate/agentTemplateUtils"
import {AgentToolSelectorPopover} from "./agentTemplate/AgentToolSelectorPopover"
Expand Down Expand Up @@ -124,6 +125,10 @@ const ModelHarnessSectionDrawerBody = ({
return <>{section === "advanced" ? mh.advancedDrawerBody : mh.modelHarnessDrawerBody}</>
}

// The four list sections whose open-state is controlled so the accordion can auto-expand when
// the agent populates them (see `useAutoExpandOnPopulate`).
const CONTROLLED_SECTION_KEYS = new Set(["tools", "mcp", "skills", "triggers"])

export function AgentTemplateControl({
schema,
value,
Expand Down Expand Up @@ -391,6 +396,30 @@ export function AgentTemplateControl({
[openCreate],
)

// Controlled open-state for the four list sections so the accordion can react to the agent
// populating a section. Seeded once from the initial counts; the edge hook below flips it.
const [sectionOpen, setSectionOpen] = useState<Record<string, boolean>>(() => ({
tools: tools.length > 0,
mcp: mcpServers.length > 0,
skills: skills.length > 0,
triggers: triggerCount > 0,
}))
const setSectionOpenByKey = useCallback(
(key: string, open: boolean) =>
setSectionOpen((m) => (m[key] === open ? m : {...m, [key]: open})),
[],
)
const sectionCounts = useMemo(
() => ({
tools: tools.length,
mcp: mcpServers.length,
skills: skills.length,
triggers: triggerCount,
}),
[tools.length, mcpServers.length, skills.length, triggerCount],
)
useAutoExpandOnPopulate(sectionCounts, setSectionOpenByKey)

// ``instructions.agents_md`` is the one instruction document (flat on the template).
const instructions =
config.instructions && typeof config.instructions === "object"
Expand Down Expand Up @@ -942,25 +971,33 @@ export function AgentTemplateControl({
))}
</div>
) : (
sections.map((s, index) => (
<ConfigAccordionSection
key={s.key}
icon={s.icon}
title={s.title}
titleBadge={sectionBadge(s.key)}
summary={s.summary}
extra={s.extra}
indicator={s.indicator ?? agentChangeIndicator(s.key)}
onOpen={s.onOpen}
defaultOpen={s.defaultOpen}
noDivider={index === sections.length - 1}
// Mount collapsed, then unfold via the normal collapse transition — first
// paint matches the skeleton's collapsed rows instead of shifting the layout.
animateInitialOpen
>
{s.content}
</ConfigAccordionSection>
))
sections.map((s, index) => {
// Controlled keys drive `open`/`onOpenChange` so the agent can auto-expand them;
// everything else keeps the mount-collapsed-then-unfold `defaultOpen` behaviour.
const controlled = CONTROLLED_SECTION_KEYS.has(s.key)
return (
<ConfigAccordionSection
key={s.key}
icon={s.icon}
title={s.title}
titleBadge={sectionBadge(s.key)}
summary={s.summary}
extra={s.extra}
indicator={s.indicator ?? agentChangeIndicator(s.key)}
onOpen={s.onOpen}
noDivider={index === sections.length - 1}
{...(controlled
? {
open: sectionOpen[s.key] ?? s.defaultOpen ?? false,
onOpenChange: (open: boolean) =>
setSectionOpenByKey(s.key, open),
}
: {defaultOpen: s.defaultOpen, animateInitialOpen: true})}
>
{s.content}
</ConfigAccordionSection>
)
})
)}

{shownEditing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {useEffect, useRef} from "react"

export type SectionCounts = Record<string, number>
export interface SectionCrossing {
key: string
open: boolean
}

/** Sections whose count crossed the 0 boundary since `prev`: 0→>0 opens, >0→0 closes. */
export function computeSectionCrossings(
prev: SectionCounts,
next: SectionCounts,
): SectionCrossing[] {
const crossings: SectionCrossing[] = []
// Union of keys so a key that vanishes from `next` still yields its >0→0 close.
for (const key of new Set([...Object.keys(prev), ...Object.keys(next)])) {
const before = prev[key] ?? 0
const now = next[key] ?? 0
if (before === 0 && now > 0) crossings.push({key, open: true})
else if (before > 0 && now === 0) crossings.push({key, open: false})
}
return crossings
}

/**
* Auto-open a list section when it goes from empty to populated (and close it when it
* empties). Edge-triggered against the previous counts, so a manual collapse of a populated
* section is never overridden by an unrelated re-render.
*/
export function useAutoExpandOnPopulate(
counts: SectionCounts,
setOpen: (key: string, open: boolean) => void,
): void {
const prev = useRef(counts)
useEffect(() => {
for (const {key, open} of computeSectionCrossings(prev.current, counts)) {
setOpen(key, open)
}
prev.current = counts
}, [counts, setOpen])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {describe, expect, it} from "vitest"

import {computeSectionCrossings} from "../../src/DrillInView/SchemaControls/agentSectionAutoExpand"

describe("computeSectionCrossings", () => {
it("opens a section on a 0 → >0 crossing", () => {
expect(computeSectionCrossings({tools: 0}, {tools: 2})).toEqual([
{key: "tools", open: true},
])
})
it("closes a section on a >0 → 0 crossing", () => {
expect(computeSectionCrossings({tools: 3}, {tools: 0})).toEqual([
{key: "tools", open: false},
])
})
it("does nothing when the count changes but does not cross 0 (manual collapse sticks)", () => {
expect(computeSectionCrossings({tools: 1}, {tools: 2})).toEqual([])
})
it("does nothing when unchanged", () => {
expect(computeSectionCrossings({tools: 2, skills: 0}, {tools: 2, skills: 0})).toEqual([])
})
it("reports each crossing key independently", () => {
expect(computeSectionCrossings({tools: 0, skills: 1}, {tools: 1, skills: 0})).toEqual([
{key: "tools", open: true},
{key: "skills", open: false},
])
})
it("closes a key present in prev but absent from next", () => {
expect(computeSectionCrossings({tools: 2}, {})).toEqual([{key: "tools", open: false}])
})
})
Loading