feat: refactor the Skills detail pane into collapsible sections with an internally-scrolling file viewer - #2265
Conversation
The pane was one scrolling column: SKILL.md sat below the manifest behind a "View SKILL.md" button, the frontmatter pushed it further out of view, and nothing could be put away. It is now a flex column that does not scroll as a whole. Conformance, Resources, Frontmatter and Skill Resource are four collapsible sections of one `disclosure` accordion, and the file viewer claims the remaining height and scrolls internally. The viewer's flex-basis is 0, not auto, and that is load-bearing: `auto` makes an item's basis its content height, and a rendered document is ~1,795px for the data-analysis fixture. That basis joined the sum the container distributes, over-constraining the column so the shrink factors crushed the siblings — Conformance to a 2px panel, Resources to zero — with their contents spilling over the headers below. Collapsing the viewer removed the giant basis, so the symptom appeared on collapse-then-reopen. Open sections also carry a `mih` floor, since the disclosure CSS sets `min-height: 0` on an active item. Frontmatter now follows the displayed file rather than the selected skill. Both halves come from one `splitSkillFile` call, so the section and the viewer cannot disagree, and a file with no frontmatter renders no section at all. Also: SKILL.md loads on selection (a `resources/read` is not a load under SEP-2640), the manifest highlights on hover with each URI a button that swaps the viewed file, both whole-skill actions moved to the pane header, and the Conformance badge goes yellow for a warnings-only entry instead of green. Flat CSS properties moved out of call sites into theme variants (ThemeText gains four; a new ThemeTable takes the manifest font size). 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>
There was a problem hiding this comment.
🟡 Changes recommended
Resource MIME/frontmatter handling and an unbounded results panel can corrupt displayed content or collapse the main accordion.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Refactors the web Skills detail pane into collapsible, independently scrolling sections with an interactive resource viewer.
Changes:
- Adds the accordion-based layout, automatic
SKILL.mdloading, and clickable manifest resources. - Splits raw frontmatter from displayed Markdown and adds supporting theme variants.
- Expands fixtures and tests for scrolling, selection, concurrency, and frontmatter behavior.
File summaries
| File | Description |
|---|---|
test-servers/src/skills.ts |
Expands the data-analysis fixture for realistic scrolling. |
clients/web/src/utils/splitSkillFile.ts |
Adds frontmatter/body splitting. |
clients/web/src/utils/splitSkillFile.test.ts |
Tests frontmatter parsing edge cases. |
clients/web/src/theme/theme.ts |
Registers the Table theme. |
clients/web/src/theme/Text.ts |
Adds Skills typography variants. |
clients/web/src/theme/Table.ts |
Adds manifest table styling. |
clients/web/src/theme/index.ts |
Exports the Table theme. |
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx |
Implements the redesigned detail pane and resource viewer. |
clients/web/src/components/screens/SkillsScreen/SkillsScreen.test.tsx |
Updates and expands screen behavior tests. |
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| const FetchResultScroll = ScrollArea.withProps({ | ||
| type: "auto", | ||
| scrollbars: "y", | ||
| offsetScrollbars: true, | ||
| h: "100%", | ||
| flex: "0 1 auto", |
There was a problem hiding this comment.
Fixed, though structurally rather than with the cap you suggested — flagging that so the difference is visible rather than implied.
You were right about the mechanism: that region's basis was its content while the accordion's was 0, so a large fetched entry starved the accordion and left its mih items overflowing. I first took the suggestion literally and added ScrollArea.Autosize with mah="50%", following ToolResultPanel's NonLinkCap.
It was then pointed out in review that the skills/get verdict is a conformance statement and belongs in the Conformance section rather than in a region of its own. Moving it there deletes the competing region entirely — FetchResultScroll and its cap are gone, and the content is now bounded by the accordion's own scrolling root. The failure mode you identified can no longer occur, because the two siblings that were competing are now one.
The same class of bug had already bitten this PR once: the file viewer originally used flex: 1 1 auto, whose content-sized basis crushed Conformance to a 2px panel and Resources to zero height. viewerFlex documents that, and it is why the finding read as credible immediately.
| const previewParts = useMemo( | ||
| () => | ||
| typeof preview?.text === "string" | ||
| ? splitSkillFile(preview.text) | ||
| : undefined, |
There was a problem hiding this comment.
Fixed as suggested — both halves of this were real.
Splitting now keys on an effective MIME rather than on the payload arm:
inferMimeFromUriis extracted out ofResourcePreviewPanelintoclients/web/src/utils/inferMimeFromUri.tsand shared by both screens, so the suffix table has one copy rather than two that can drift. The Skills screen resolvespreview.mimeType ?? inferMimeFromUri(uri) ?? "text/markdown"— markdown as the last resort rather than octet-stream, since skill files are markdown by construction under SEP-2640.- Only a markdown resource is split, so a textual multi-document YAML file keeps its first document.
- A markdown resource served as a base64
blobis decoded withtryDecodeBase64ToUtf8and split like a text one, so it loses its frontmatter to the Frontmatter section and does not keep it in the viewer.
One consequence worth naming: the split and the Frontmatter section are now driven by a single splitSkillFile call, so the section cannot show one file's frontmatter beside another file's body, and a file with no frontmatter renders no section at all rather than leaving the previous file's on screen. Covered by splitSkillFile.test.ts (fence-at-start only, unterminated fence returns the file whole, CRLF, frontmatter-only) and by a screen test that switches between a file with frontmatter and one without.
Review on #2265 surfaced that the pane reported judgement from three sources in three different places: static findings in Conformance, per-file digest verification beside the manifest table, and the `skills/get` comparison in a region of its own. All three now render in Conformance. That also resolves the first Copilot finding structurally rather than with the cap it suggested. The free-standing `skills/get` region had a content-sized basis competing with an accordion whose basis was 0, so a large fetched entry starved the accordion; moving the verdict inside deletes the competing region entirely, and `FetchResultScroll` goes with it. A red `N digest mismatch(es)` badge joins the errors/warnings badge, kept separate because it is a runtime result — folding it into the error count would make that number change meaning after a click. Conformance auto-collapses for an entry with no findings and auto-opens whenever a verification, a fetch or a new selection gives it something to say. Its badge is yellow for a warnings-only entry; green read as "nothing to see". A dynamic skill states its case once: the prose banner moves into Conformance and the Resources section is dropped entirely, while the finding still counts toward the warning total. The second Copilot finding is fixed as suggested. Frontmatter splitting keyed on the payload arm rather than the MIME, so a markdown blob kept its frontmatter while a multi-document YAML file lost its first document. It now keys on an effective MIME, with `inferMimeFromUri` extracted to `src/utils/` and shared with `ResourcePreviewPanel` instead of duplicated. Also: `Verify` gets its own table column so the buttons align rather than tracking the verdict badge's width; the header actions take `ListChangedIndicator`'s Refresh style and expand/collapse-all uses the shared `ListToggle`; the `skills/get` copy no longer implies the entry defines the rule it breaks; findings move below the JSON they annotate; and section spacing moves into the `skillSections` accordion variant. 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>
Copilot review round 1 — both findings addressedMirrored here because inline replies get hidden once the fixing commit is pushed. 1. Unbounded The mechanism was exactly as described: that region's basis was its content while the accordion's was Worth noting the same class of bug had already bitten this PR: the file viewer originally used 2. Frontmatter split keyed on the payload arm rather than the resource type — fixed as suggested. Splitting now keys on an effective MIME. Also in this push, from maintainer reviewThe pane reported judgement from three sources in three places; all three now render in Conformance, with a red Two defects the gate caught, recorded so they are not re-litigated
|
There was a problem hiding this comment.
🟡 Changes recommended
MIME handling can misrender valid resources, mismatch labeling is inaccurate, and the central layout regression lacks browser coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 4
- Review effort level: Balanced
Four findings, all real. The `text/markdown` fallback applied to every manifest resource, but SEP-2640 expects a manifest to carry supporting scripts, examples and assets with types of their own — an extensionless, untyped blob was being decoded and rendered as markdown rather than falling back to binary. The fallback is now scoped to the skill's own SKILL.md; anything else passes `undefined` and lets ContentViewer apply its octet-stream/text default. `mismatchCount` counts `status: "mismatch"`, which `verifySkillResource` uses for both digest AND size failures, so the badge misreported a size-only failure as a digest mismatch. Renamed to "N mismatch(es)" rather than filtering: a size failure is a real verification failure and dropping it from the count would hide it. The alerts still distinguish the two by title. `isMarkdownMime` compared the raw string, so `text/markdown; charset=utf-8` and any casing variant were rejected. That silently skipped the frontmatter split — the fence stayed in the rendered document and the Frontmatter section vanished, for a perfectly valid response. It now normalises to the base lowercase type. The flex-basis regression this refactor exists to fix had no browser-level guard: the unit tests only inspect accordion state, and the long test-server fixture is never exercised by Storybook. `LongSkillDocument` is modelled on `ResourcesScreen`'s `ManyResources` and asserts that every section keeps a non-zero height, that sections tile without overlapping the header below, that the viewer scrolls within its own panel, that the detail pane does not scroll as one column, and that collapse-then-reopen restores identical geometry — the sequence the original bug needed in order to show itself. 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>
Copilot review round 2 — all four findings addressed (
|
There was a problem hiding this comment.
🟡 Changes recommended
The detail column lacks a definite height, and generic MIME declarations prevent valid Markdown frontmatter from being split.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 2
- Review effort level: Balanced
Copilot review round 3. Precedence was `declared ?? inferred`, so a server serving SKILL.md as `text/plain` or `application/octet-stream` defeated the `.md` inference — and `ResourcePreviewPanel` already records that servers commonly do exactly that. The file was then not recognised as markdown, so its YAML frontmatter stayed in the viewer and the Frontmatter section disappeared, for a perfectly valid skill. A SPECIFIC declared type still wins, because a server that says `text/csv` knows its own resource. Only the generic types yield to the suffix, via a new `isGenericMime` that normalises parameters and casing the same way `isMarkdownMime` does. Both directions are covered: `text/plain` on a `.md` URI splits, `text/csv` on a `.md` URI does not. The round's other finding — that `DetailCard` is not a flex container, so the detail column has no definite height — is declined as a false positive, with evidence on the thread: Mantine's own Card.css sets `display: flex; flex-direction: column` on the card root, `DetailCard` still carries `h: "100%"`, and the `LongSkillDocument` story added last round asserts the exact behaviour claimed broken, in a real browser. 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>
Copilot review round 3 — one fixed, one declined (
|
There was a problem hiding this comment.
🔵 Needs a closer look
Initial clean selections retain incorrect accordion state, and the Resources panel contains an unreachable branch.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:542
useValueChangeintentionally skips the first render, so this all-open initializer leaves Conformance expanded when the screen mounts with an already-selected clean skill (for example, a restoredSkillsUiState). The promised clean-entry auto-collapse only works after a later selection change. Seed this state from the initially selected entry’s findings, then keep the existing render-time synchronization for subsequent changes; cover the initially-selected case directly.
This issue also appears on line 1311 of the same file.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:1311
- This condition is unreachable because the enclosing render guard is
!isDynamic, whileisDynamicis defined by this exact comparison. Remove the dead dynamic branch and render the manifest table directly; the dynamic explanation already lives in Conformance as intended.
{selected.resources === DYNAMIC_RESOURCES ? (
- Files reviewed: 14/14 changed files
- Comments generated: 0 new
- Review effort level: Balanced
…2263) Copilot review round 4. Both findings arrived in the review's *suppressed* block with zero inline comments, so a round that reported "0 new comments" was in fact carrying two real defects. `useValueChange` deliberately does not fire on the first render, so the clean-entry auto-collapse it drives never applied to a screen that MOUNTS with a skill already selected — a restored `SkillsUiState` does exactly that, and Conformance would sit expanded until some later selection change started honouring the rule. The rule was written entirely in the change handler and the initial state was never considered. `openSections` now seeds through `initialOpenSections`, applying the same judgement up front while the render-time synchronisation continues to handle every subsequent change. The Resources panel still held `selected.resources === DYNAMIC_RESOURCES ? … : …` inside a `{!isDynamic && …}` guard, and `isDynamic` is that exact comparison, so the alert arm was unreachable. It is residue from moving the dynamic banner into Conformance: the outer guard was added and the inner ternary never removed. The manifest table now renders directly. 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>
Copilot review round 4 — both findings addressed (
|
There was a problem hiding this comment.
🟡 Changes recommended
Non-shrinking metadata sections can push the resource viewer below large valid manifests.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 1
- Review effort level: Balanced
Copilot review round 5, and it caught a fix that traded one bug for another. Refusing to shrink (`0 0 auto`) was introduced to stop a squeezed section slicing its content mid-line. It did that, and reintroduced the original defect at scale: a CONFORMING manifest may declare 512 rows, and a section holding its full intrinsic height pushes the file viewer off the bottom of the pane, so reaching the file means scrolling past the manifest — precisely the "the file is behind the manifest" problem this screen was refactored to end. `0 1 auto` with the existing `OPEN_SECTION_MIN_HEIGHT` floor satisfies both: a section gives up space until it reaches the floor, its panel scrolls internally from there, and the viewer keeps the remainder. The floor is what stops the shrink becoming the crush that sliced content — a panel at the floor scrolls rather than clips. Both wrong directions are now recorded on the constant so neither is re-derived. `LongManifest` is the sibling story to `LongSkillDocument`, asserting that the viewer's header stays within the card, that the Resources panel scrolls internally, and that the pane does not scroll as one column. Verified to detect the defect rather than merely pass: with `0 0 auto` restored it fails while the other eight stories pass. 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>
Copilot review round 5 — addressed (
|
SECTION_FLEX |
Long Manifest |
other 8 stories |
|---|---|---|
0 1 auto (fixed) |
pass | pass |
0 0 auto (defect) |
fail | pass |
npm run local:gate passes.
Where this review stands
| round | findings | outcome |
|---|---|---|
| 1 | 2 | both fixed |
| 2 | 4 | all fixed |
| 3 | 2 | 1 fixed, 1 declined with evidence (Mantine's own Card.css supplies the flex container) |
| 4 | 2 (suppressed block; 0 inline) | both fixed |
| 5 | 1 | fixed |
11 findings, 10 fixed. Every fix gated before pushing.
There was a problem hiding this comment.
🟡 Changes recommended
An unbounded resource filename can still disrupt the accordion layout, and one text constant bypasses the theme-variant convention.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:297
size="sm"leaves typography in the component even though this refactor moves Text typography intoThemeTextvariants (seeclients/web/src/theme/Text.ts:54-79) and the comment above says these constants carry layout only. Add askillDescriptionText variant and keep only the clamping behavior here.
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Balanced
…heme (#2263) Copilot review round 9, two findings. The Skill Resource control shows the displayed file's name, and that control is PINNED — it never scrolls — so a resource URI ending in a long breakable segment grew the header. Fifth instance of the same class. It took three attempts, and the mutation table is what exposed each one. `lineClamp: 1` stopped the text wrapping but the row then wrapped the caption onto its own line (+30.6px), because flexbox wraps before it shrinks. A `nowrap` row improved that to +21.7px but Mantine's Accordion label wrapper still would not let the caption shrink, so `miw: 0` had nothing to act on. A hard `maw: "50%"` gives the ellipsis a definite width, and the control now grows by 0px. Worth recording that the FIRST version of that guard passed against the defect: it asserted an absolute `height < 120px`, which an unclamped name satisfies because it only wraps to a few lines. Comparing the control against its own baseline — measured with a short name, then re-measured after selecting the hostile one — is what makes it detect anything at all. Second finding: `SkillDescription` carried `size: "sm"`, leaving typography at the call site directly beneath a comment of mine asserting that these constants carry layout only and that typography belongs in a `ThemeText` variant. Added a `skillDescription` variant; only the clamp stays here. 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>
Copilot review round 9 — both addressed (
|
| attempt | control growth |
|---|---|
lineClamp: 1 |
+30.6px — text stopped wrapping, the row wrapped the caption onto its own line |
+ nowrap row |
+21.7px — Mantine's Accordion label wrapper still would not let it shrink |
+ maw: "50%" |
0px |
Flexbox wraps before it shrinks, and min-width: 0 only helps when every ancestor also permits shrinking; a hard cap gives the ellipsis a definite width regardless.
⚠️ The first version of that guard passed against the defect — it asserted an absoluteheight < 120px, which an unclamped name satisfies because it wraps to a few lines, not dozens. It now measures the control with a short name and asserts the height is unchanged after selecting the hostile one. Without re-running it against the un-fixed code I would have reported a regression test that proved nothing.
2. SkillDescription kept size: "sm" at the call site — typography left in the component, directly beneath a comment of mine asserting that these constants carry layout only and that typography belongs in a ThemeText variant. Added a skillDescription variant; only the clamp stays.
npm run local:gate passes. 495 unit tests, 10 stories.
| round | findings | outcome |
|---|---|---|
| 1 | 2 | both fixed |
| 2 | 4 | all fixed |
| 3 | 2 | 1 fixed, 1 declined with evidence |
| 4 | 2 (suppressed; 0 inline) | both fixed |
| 5 | 1 | fixed |
| 6 | 4 | all fixed |
| 7 | 4 | all fixed |
| 8 | 1 | fixed, + 1 self-found a11y defect |
| 9 | 2 | both fixed |
22 findings, 21 fixed.
There was a problem hiding this comment.
🔵 Needs a closer look
Raw frontmatter can be reformatted and the consolidated verdict UI contains contradictory or incomplete semantics.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:1583
- This does not always display the served YAML verbatim.
ContentViewerroutestext/yamlthrough its plain-text branch, which auto-detects and pretty-prints JSON (ContentViewer.tsx:256-271). Since JSON mappings are valid YAML, frontmatter such as{"name":"a","description":"b"}is reserialized despite this section promising raw bytes. Use a raw-text mode/renderer that disables the JSON heuristic for frontmatter.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:1273 - After a digest/size mismatch, this section shows a green “Conforms” alert alongside the red runtime failure. Now that all verdicts live in Conformance, the unqualified title contradicts the verification result; use “No structural issues” so it accurately describes only the static check summarized by the body.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:1517 - The PR description says the shield remains on both integrity-checking controls, but the per-row
RowVerifyButtonstill renders only the text label. AddMdVerifiedUserto the shared row-button definition so the stated icon semantics are applied consistently to “Verify all” and each row’s “Verify”.
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Zero inline comments, three findings in the review's suppressed block — the
second round where the inline count alone would have ended the cycle. All three
are places where my own words did not match the code.
The Frontmatter section claimed to show "the raw YAML the server served, not a
re-serialised object". It did not: `ContentViewer`'s plain-text branch
pretty-prints anything that parses as JSON, and every JSON mapping is also valid
YAML — so `{"name":"a"}` came back re-serialised beneath a comment promising
verbatim bytes. For a conformance tool that is the one thing it must not do. It
now renders through `CodeHighlight` directly.
The green alert was titled "Conforms". Consolidating every verdict into this one
section means it can sit directly above a red digest mismatch, where an
unqualified "Conforms" contradicts the result on screen — the static checks
passing says nothing about the bytes. Retitled "No structural issues", which is
what it actually summarises. The contradiction was created by the consolidation
and the title was never revisited.
And the PR description said the shield stays on both integrity-checking
controls, while the per-row Verify rendered text only. The claim was right and
the code was not, so the row button now carries it too.
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>
Copilot review round 10 — all three addressed (
|
| round | findings | outcome |
|---|---|---|
| 1 | 2 | both fixed |
| 2 | 4 | all fixed |
| 3 | 2 | 1 fixed, 1 declined with evidence |
| 4 | 2 (suppressed; 0 inline) | both fixed |
| 5 | 1 | fixed |
| 6 | 4 | all fixed |
| 7 | 4 | all fixed |
| 8 | 1 | fixed, + 1 self-found a11y defect |
| 9 | 2 | both fixed |
| 10 | 3 (suppressed; 0 inline) | all fixed |
25 findings, 24 fixed.
There was a problem hiding this comment.
🔵 Needs a closer look
StrictMode duplicates automatic reads, and scroll positions persist across skill changes.
Review details
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:912
SkillsScreenruns underStrictMode(main.tsx:13) andScreenStageremounts it while the selected-skill UI state persists. On such a remount this effect is replayed, so it issues two identicalresources/readRPCs and records duplicate Protocol entries. Track the automatically loaded manifest key in a ref so the StrictMode replay is a no-op.
This issue also appears on line 1225 of the same file.
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:1226
- The accordion and its native scroll containers are reused when the selected skill changes, so a long Resources or Conformance panel retains its old
scrollTop. Switching to another long skill can therefore show its manifest/findings partway down and hide the beginning. Keying the accordion bymanifestKeyresets those DOM scroll positions while preserving the controlledopenSectionsstate.
<Accordion
multiple
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
…#2263) Copilot review round 11: zero inline comments, two findings in the suppressed block. Third round where the inline count alone would have ended the cycle. The automatic SKILL.md read fired twice. I noted this possibility early and dismissed it as harmless dev noise, which was wrong twice over: `ScreenStage` remounts this screen while the selection persists, so it is not dev-only, and this app is a protocol inspector — a phantom `resources/read` in the Protocol panel that the user's own client would never send is the tool misreporting the conversation, not merely a wasted round trip. An `autoReadKey` ref makes the replay a no-op. Panel scroll positions survived a skill change. `scrollTop` lives on the DOM node rather than in React state, so switching between two long manifests showed the new one part-way down with its first rows hidden, which reads as missing data. The accordion is keyed by `manifestKey` so every panel gets a fresh scroll container; `openSections` is controlled and survives.⚠️ The test for the first fix is a SPECIFICATION, not a regression guard, and is labelled as such in the file. It was written, then checked by removing the guard — and it still passed. Three shapes were tried (a plain re-render, a StrictMode wrapper, StrictMode with the selection present at mount) and none reproduce the replay, because this environment does not double-invoke mount effects. The fix stands on the reported reasoning; the test pins the contract of one read per selection and must not be read as evidence the defect is fixed. 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>
Copilot review round 11 — both addressed (
|
| test shape | detects missing guard? |
|---|---|
| plain re-render with identical props | no |
wrapped in <StrictMode>, select after mount |
no |
wrapped in <StrictMode>, selection present at mount |
no |
This environment does not double-invoke mount effects, so the replay cannot be reproduced here; the guard is only observable in a real dev-mode browser. The test therefore pins the contract — one automatic read per selection — and carries a comment saying explicitly that a pass is not evidence the duplicate-read defect is fixed. The fix stands on the reasoning in the finding, which I agree with.
npm run local:gate passes. 496 unit tests, 10 stories.
| round | findings | outcome |
|---|---|---|
| 1–3 | 8 | 7 fixed, 1 declined with evidence |
| 4 | 2 (suppressed; 0 inline) | both fixed |
| 5–7 | 9 | all fixed |
| 8 | 1 | fixed, + 1 self-found a11y defect |
| 9 | 2 | both fixed |
| 10 | 3 (suppressed; 0 inline) | all fixed |
| 11 | 2 (suppressed; 0 inline) | both fixed |
27 findings, 26 fixed.
There was a problem hiding this comment.
🟡 Changes recommended
Automatic-read state is mishandled across screen remounts and temporarily missing selected entries.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
clients/web/src/components/screens/SkillsScreen/SkillsScreen.tsx:927
- When the selected skill temporarily disappears from
skills,selectedUribecomes undefined and the render invalidates the preview, but this return leaves the old manifest key cached. If the identical entry later reappears, its key matches the cache, so no read runs and the viewer remains empty. Reset the guard while no selected entry exists.
useEffect(() => {
if (selectedUri === undefined) return;
if (autoReadKey.current === manifestKey) return;
autoReadKey.current = manifestKey;
showResource(selectedUri, manifestKey);
}, [manifestKey, selectedUri, showResource]);
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Balanced
…list `autoReadKey` cached the manifest key of the entry it had read for. When the selected entry left `skills` — a refresh in flight, a disconnect — the render emptied the preview but the ref kept the key. An identical entry reappearing therefore matched the cache, the automatic read was skipped, and the viewer stayed blank with no recovery short of reselecting. Clearing the guard while no entry is selected is what lets the reappearance re-read. Also corrects the guard's stated scope. It cannot survive a `ScreenStage` remount — a remount mints a fresh ref — so the round-11 comment claiming it did was wrong. That read is correct behavior rather than a gap: `previewState` is local state and dies with the same unmount, so on return the viewer is empty and the read is what refills it. Suppressing it would mean hoisting the preview bytes above `ScreenStage` and showing bytes fetched under a connection that may no longer exist. The reappearance test is a real regression guard: restoring the defect fails it while the older one-read-per-selection test still passes, so it is both detecting and non-redundant. That older test remains a specification, for the reason it already states, and now points at this one. 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>
Copilot review round 12 — both addressedOne inline comment and one suppressed, both on the same eight lines. Both are correct, and the first one retracts a claim I made in round 11. 1.
|
| test | with fix | with defect restored |
|---|---|---|
re-reads when the selected entry leaves the list and comes back |
pass | fail |
issues exactly one automatic read per selection within a mount |
pass | pass |
The second row is the non-redundancy check — the older test does not cover this path, so the new one is carrying its own weight. The older test remains a specification for the reason stated in round 11 and now says so while pointing at the new test as the actual guard.
npm run local:gate passes.
| round | findings | outcome |
|---|---|---|
| 1–3 | 8 | 7 fixed, 1 declined with evidence |
| 4 | 2 (suppressed; 0 inline) | both fixed |
| 5–7 | 9 | all fixed |
| 8 | 1 | fixed, + 1 self-found a11y defect |
| 9 | 2 | both fixed |
| 10 | 3 (suppressed; 0 inline) | all fixed |
| 11 | 2 (suppressed; 0 inline) | both fixed |
| 12 | 2 (1 inline, 1 suppressed) | both fixed; one retracts a round-11 claim |
29 findings, 28 fixed.
There was a problem hiding this comment.
🟢 Approval recommended
The implementation satisfies the issue scope with comprehensive unit and browser-level regression coverage and no unresolved defects found.
Review details
- Files reviewed: 15/15 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Review cycle complete — two consecutive clean approvalsRounds 13 and 14 both came back 🟢 Approval recommended, 0 comments, no suppressed block, on the identical tree — nothing was pushed between them, so the second is a confirmation rather than a re-review of new code. Two rounds rather than one because on this PR the inline comments have posted after the body more than once, and three of the last six rounds carried their only findings in the Suppressed comments block while the body read as clean. Final tally
29 findings, 28 fixed, 1 declined with evidence — the round-3 The two findings worth rememberingSizing. Five separate defects were the same shape: any server-controlled variable-height content that is a sibling of the zero-basis viewer sizes the layout. Guards. Two tests in this PR looked like regression guards and were not, and mutation testing is the only thing that told them apart.
|
Closes #2263
Refactors the Skills detail pane. Before this, the whole pane was one
DetailScrollcolumn: theSKILL.mdsat below the manifest table behind a View SKILL.md button, the frontmatter block pushed it further out of view, and nothing could be put away.Now the pane is a flex column that does not scroll as a whole. Conformance, Resources, Frontmatter and Skill Resource are four collapsible sections of one accordion, and the file viewer claims whatever height the others leave and scrolls internally.
The layout bug this had to solve
The obvious implementation — give the viewer
flex: 1 1 autoso it fills the remainder — is wrong, and wrong in a way that only shows up with a realistic file.flex-basis: automakes an item's basis its content height, and this panel's content is a rendered document: 1,795px for thedata-analysisfixture. That basis joins the sum the container distributes, the column becomes wildly over-constrained, and the shrink factors crush the siblings. Measured:Conformance got a 2px panel and Resources zero height, both spilling over the headers below. Collapsing the viewer removed the giant basis and everything laid out correctly, so the symptom appeared on collapse-then-reopen rather than on load.
Two things fix it, and both are load-bearing:
viewerFlexgives the viewer a basis of0, so it contributes nothing to that sum and simply takes what the others leave.mihfloor. ThedisclosureCSS setsmin-height: 0on an active item — necessary for its panel to scroll rather than overflow, but also what permits the collapse to nothing. A Mantinemihprop is an inline style, so it bounds the shrink without touching the shared stylesheet.Verified by measurement rather than by eye — the four section heights sum to exactly the container height in both configurations, and collapse→reopen now returns to identical geometry:
Frontmatter follows the displayed file
The Frontmatter section used to render
selected.frontmatter— a property of the skill, not of the file on screen — so switching toreference.mdleftSKILL.md's fields sitting there.Both halves now come from one
splitSkillFile(text)call: the section renders thefrontmatterhalf, the viewer renders thebodyhalf. Deriving both from a single split is what makes them incapable of disagreeing, and a file with no frontmatter renders no section at all rather than leaving a stale one behind.It shows the raw YAML the server served, not a re-serialised object — this repo carries no YAML parser, and for a conformance tool the bytes on the wire are the more useful answer anyway: a file that disagrees with what the server listed is exactly what you want to see.
Two conservatisms, because a display helper must never eat content: only a fence at the very start of the file counts (a
---elsewhere is a horizontal rule), and a file that opens with---but never closes the fence is not frontmatter and is returned whole rather than truncated to nothing.The rest
View SKILL.mdis gone. The skill's own file loads on selection. SEP-2640 is explicit that aresources/readof aSKILL.mdis not a load and confers no standing, so this claims nothing on the user's behalf — but it is a deliberate change from Support the Skills extension (SEP-2640): skills/list, skills/get, and digest verification #2234's "fetch only what is asked for" posture, called out here rather than absorbed. The read is an effect;useValueChangestill drops the previous skill's results during render, so there is no stale frame for the effect to fix.highlightOnHover, and each URI is a full-width, left-aligned button that swaps the file in the viewer. The row on display is markedaria-current.Skill Resource, with the displayed file's name to its right, so it no longer changes shape as the file changes.Fetch with skills/getandVerify allact on the skill rather than on a section — and a button inside anAccordion.Controlwould toggle that section on its way to firing.Verify allis now reachable with Resources collapsed.Fetch with skills/get. It is a claim about integrity, so it stays on the two controls that actually check it (Verify alland the per-rowVerify).skills/getre-fetches an entry and compares it against the listing — a consistency check, not a digest verification.errorCount === 0, which reads as "nothing to see" and hid the only signal the section carries for adynamic-resourcesorsize-limit-exceededfinding.Mantine conventions
Reviewed against the component rules in
AGENTS.mdwhile the screen was open, including code that predates this issue:ThemeTextgainssectionHeading,skillTitle,monoCaptionandemptyState; a newsrc/theme/Table.ts(ThemeTable,manifestvariant) takes the manifest's font size. The.withProps()constants now carry layout only.disclosurevariant (ResourceControls: sections scroll prematurely (equal height-division) + accordion chevrons point up/down instead of right/down #1462, asResourceControlsdoes) rather than a bespoke scroll region — pinned headers, per-panel scrolling, and the same chevron and hover treatment.fullWidth,justify,size); the frontmatter block is wrapped in aFramedContent(Paper,withBorder) rather than styled in place.splitSkillFilelives insrc/utils/— a pure transform with no I/O, and a component file that also exports a function defeats React Fast Refresh.Test servers
data-analysis'sSKILL.mdgrows from two lines to a ~60-line document. Not decoration: every file in that fixture was short enough that the viewer never scrolled, so a regression that let the whole pane scroll again would have looked identical to the fix. Digest and size are both derived from the text, so the fixture stays self-consistent and still verifies green.Testing
npm run local:gatepasses. New and changed code clears ≥90 on all four coverage dimensions.Conformance is the one place a verdict is reported
The screen had three sources of judgement rendered in three different places: static findings in Conformance, per-file digest verification beside the manifest table, and the
skills/getcomparison in a region of its own. They now all render in the Conformance section.That is what resolves Copilot's first finding structurally rather than cosmetically — see the review thread.
N digest mismatch(es)badge joins the errors/warnings badge in the header. Kept separate because it is a runtime result; folding it into the error count would make that number change meaning after a click. It appears only once something has failed, since a standing "0 digest mismatches" would read as a verified result before anything had been checked.tampered-notesis the case that forces this: structurally clean, bytes wrong.dynamic-resourcesorsize-limit-exceededfinding carries.A dynamic skill states its case once
resources: "dynamic"produced two banners saying the same thing, plus an empty Resources section whose only content explained its own emptiness. The prose banner now renders in Conformance and the Resources section is dropped entirely — header and all, not merely emptied. The finding still counts toward the warning total, because suppressing the count would make a dynamic skill indistinguishable from a verified one.Copy and ordering
skills/getentry's findings moved below the JSON they annotate, so each one reads as an annotation on what you have just been shown.Table and controls
Verifyhas its own column. Sharing a cell with the verdict badge staggered the buttons, because badge width tracks its label ——,checking…,verifiedandmismatchare all different sizes.ListChangedIndicator's Refresh style (sm+subtle), and the sidebar Refresh loses a glyph no other list has.ListToggleelement — the same oneResourceControlsputs on its disclosure accordion — rather than a bespoke button.Before / After
Before. One scrolling column: the manifest, then the
skills/getresult, thenSKILL.mdbehind a View SKILL.md button, then the frontmatter — everything competing for the same scroll.After. Collapsible sections, the file viewer taking the remaining height and scrolling on its own, and the frontmatter shown once — split out of the file on display rather than repeated inside it.
Screenshots
Captured headlessly against the built prod bundle connected to
test-servers/configs/skills-http.json— the views #2251 documented, re-shot against the refactored pane.The Skills tab. Four skills over two
skills/listpages; the Protocol panel shows both calls, which is what proves the cursor walk ran.A conforming skill, verified. Both files hash to the digests the manifest advertised. Conformance is collapsed here — the entry is clean, and its badge says so.
skills/get, the extension's second required method. The verdict renders in Conformance, which the fetch opens; the findings sit below the entry they annotate.A digest mismatch.
notes.mdadvertises a well-formed digest of bytes the server does not serve. The row badge flips toMISMATCH, the header gains1 DIGEST MISMATCH(ES), and the alert names the file with both digests.resources: "dynamic". One banner, in Conformance, and no Resources section at all. The badge is yellow — the entry's one finding is a warning.A name/path disagreement. Served from
wrong-folder/while claiming the nameright-name— the one structural invariant SEP-2640 states outright.Connection Info. Unchanged by this PR, included for parity with #2251's set.
🤖 Generated with Claude Code
https://claude.ai/code/session_01R4441JdNLZerCVM7mDDSfB