fix: address Copilot review round 6 (#2263)

Four findings — two inline, two in the review's suppressed block.

"Expand all" wrote `sectionIds` — the sections rendered at that instant — into
`openSections`, which outlives any one skill. Pressing it while the SKILL.md
read was still in flight dropped `frontmatter`, so the section arrived collapsed
with the control offering to expand all over again; a dynamic skill did the same
to `resources`. It now expands to the complete set, and `sectionIds` decides
only the toggle's label. The new test is verified to detect the defect: with
`sectionIds` restored it fails.

`inferMimeFromUri` matched the raw path, so a percent-encoded suffix such as
`reference%2Emd` produced no MIME at all — no markdown renderer and no
frontmatter split — even though `skillUriIdentity` already treats that spelling
as the same file. It now decodes defensively, falling back to the raw path on a
malformed escape rather than throwing inside what is only a guess.

Two comment fixes, both cases of a deliberate decision being contradicted by the
prose around it. `theme/Accordion.ts` still asserted "sized to content and never
shrink; overflow moves to the root" after round 5 moved the sections back to
`flex: 0 1 auto` with a floor — the exact stale rationale that makes a
load-bearing layout easy to regress. And the component doc, the
`onReadSkillFile` doc in `useServerCommands.tsx` and a `test-servers` fixture
comment all still said skill files are read only when the user asks to verify
them, one of them still naming the removed "View SKILL.md" button. All three now
separate reading (on selection, and not a load under SEP-2640) from verification
(strictly on demand).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R4441JdNLZerCVM7mDDSfB
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-06 01:21:54 -04:00
commit d78089892b
2 changed files with 139 additions and 22 deletions

View file

@ -1263,6 +1263,53 @@ describe("SkillsScreen", () => {
);
});
it("expand-all covers sections that are not visible yet", async () => {
// `sectionIds` holds only what renders at this instant, and "expand all"
// used to write exactly that — so a section absent at the moment of the
// click (Frontmatter, while the read is still in flight; Resources, on a
// dynamic skill) was DROPPED from the open set, and arrived collapsed with
// the control offering to expand all over again (#2263).
const user = userEvent.setup();
renderWithMantine(<ControlledSkillsScreen />);
// Start on the dynamic skill, which renders no Resources section at all.
await user.click(screen.getByText("dynamic-report"));
// Settle the auto-read before touching the toggle, so the click lands on a
// known state rather than racing the section set.
await waitFor(() =>
expect(
screen.getByRole("button", { name: /Collapse all|Expand all/ }),
).toBeInTheDocument(),
);
const toggle = () =>
screen.getByRole("button", { name: /Collapse all|Expand all/ });
if (toggle().getAttribute("aria-label") === "Collapse all") {
await user.click(toggle());
}
await user.click(screen.getByRole("button", { name: "Expand all" }));
// Switch to a static skill WITH findings, so the clean-entry collapse rule
// does not overlap with what this test is about. Scoped to the sidebar:
// with every section expanded, the skill's own name also appears in the
// detail pane's frontmatter block.
await user.click(
within(screen.getByTestId("skills-screen")).getAllByText("right-name")[0],
);
await waitFor(() =>
expect(screen.getByRole("button", { name: /Resources/ })).toHaveAttribute(
"aria-expanded",
"true",
),
);
expect(screen.getByRole("button", { name: /Frontmatter/ })).toHaveAttribute(
"aria-expanded",
"true",
);
// And the control agrees that everything is open.
expect(
screen.getByRole("button", { name: "Collapse all" }),
).toBeInTheDocument();
});
it("toggles every section at once from the header control", async () => {
const user = userEvent.setup();
renderWithMantine(<ControlledSkillsScreen />);

View file

@ -518,9 +518,14 @@ function shortDigest(digest: string | undefined): string {
* The sidebar lists the skills the server enumerated; the detail pane shows the
* entry's frontmatter, every conformance finding
* (`checkSkillConformance`), and the resource manifest with a per-file
* verification verdict. Verification is on demand: SEP-2640 says a
* `resources/read` of a skill file is not a load and confers no standing, so
* the Inspector fetches only what the user asks it to.
* verification verdict.
*
* **Reading is not verification, and the two have different triggers** (#2263).
* The skill's own `SKILL.md` is read as soon as it is selected, so the file is
* simply on screen; SEP-2640 is explicit that a `resources/read` of a skill
* file is *not* a load and confers no standing, so that claims nothing on the
* user's behalf. Digest **verification** remains strictly on demand — it is
* what the buttons are for, and nothing is hashed until asked.
*/
export function SkillsScreen({
sessionKey,
@ -963,11 +968,20 @@ export function SkillsScreen({
// Which sections this skill actually renders — Frontmatter only exists when
// the displayed file has any, so an "expand all" that named it unconditionally
// would leave the toggle stuck reading "Expand" on a file without one.
// The sections rendered RIGHT NOW. A dynamic skill renders no Resources
// section, and Frontmatter is absent until the file has been read and turns
// out to have any — so naming those unconditionally would leave "expand all"
// permanently unsatisfied.
//
// This drives the toggle's *label* only. It must NOT be what "expand all"
// writes: `openSections` outlives any one skill, so replacing it with the
// currently-visible set silently drops the others. Pressing Expand all while
// the SKILL.md read was still in flight removed `frontmatter`, and the
// section then arrived collapsed with the control offering to expand all over
// again; switching away from a dynamic skill did the same to `resources`.
const sectionIds = useMemo(
() => [
"conformance",
// A dynamic skill renders no Resources section, so naming it here would
// leave "expand all" permanently unsatisfied.
...(isDynamic ? [] : ["resources"]),
...(previewParts?.frontmatter !== undefined ? ["frontmatter"] : []),
"resource",
@ -1099,8 +1113,10 @@ export function SkillsScreen({
<ListToggle
variant="subtle"
compact={!allSectionsOpen}
// Expands to the COMPLETE section set, not the visible one —
// see `sectionIds` for what writing the visible set dropped.
onToggle={() =>
setOpenSections(allSectionsOpen ? [] : [...sectionIds])
setOpenSections(allSectionsOpen ? [] : [...ALL_SECTIONS])
}
/>
</InlineRow>