Skip to content

feat(studio): show a skill's files on its page - #2155

Merged
peppescg merged 2 commits into
mainfrom
issue-1853-placeholders
Oct 15, 2026
Merged

peppescg merged 2 commits into
mainfrom
issue-1853-placeholders

Conversation

@kantord

@kantord kantord commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

  • The configured skill page's Files section lists the skill's files, each with its size and syntax-highlighted text (Studio's existing Shiki component, chosen by file extension) in its own scrolling block that fades at the edges it can still scroll toward.
  • The first entry is the skill's instructions, labelled "Instructions, without frontmatter". The daemon lists the text after the frontmatter under the name SKILL.md, so it is not the raw file and its size is the body's size. Showing the frontmatter fields (allowed-tools, license) is tracked in Skills: expose frontmatter metadata to clients and reconcile the skill page with the latest prototypes #2301.
  • A file that cannot be shown stays listed with a one-line reason and never hides the others: "Too large to show here." (over the daemon's 25,000-byte cap), "This is a binary file, so it can't be shown here.", or "Couldn't load this file." for any other failure, such as a rate-limited daemon.
  • Adds the BFF route GET /api/v1/skills/{name}/files. It loads the files' text in one response, at most four daemon reads at a time, without requesting a file already listed as over the cap, and only the first 32 files; the page then says "N more files aren't shown." The route answers 501 skill_files_unsupported unless the daemon announces the skill_files feature.
  • The page starts the files request as soon as it opens, without waiting for the skill inventory.
  • Adds the vertical scroll-fade-y utility from shadcn/ui's tailwind.css (MIT) to styles.css. Browsers without scroll-driven animations get no fade, unlike shadcn's fallback, which fades both edges of every block even when it does not scroll.
  • There is no Manage section. The baseline's disabled Edit and Disable/Delete controls wait for a write contract (Define a write contract for configured skills #2184, Studio skills: manage configured skills (create, edit, disable, delete) #2185).

This is the last piece of #1853's in-scope work. It replaces the earlier placeholder-only version of this PR.

Depends on

#2192 (merged), which added the daemon RPCs, the HTTP routes, and the SDK methods skills.listFiles and skills.readFile. Studio's BFF builds against the checked-out SDK (file:../../sdk/typescript; see #2288), so it uses them with no version bump. They are also in the published @stacklok-oss/mecatl-sdk 0.5.0.

Issue relationship

Closes #1853

Real Manage (writes) is tracked in #2184 and #2185, slash and mention completion in #2160, whether learned skills are usable in #2168, and the skill's frontmatter fields, how large skills load, and the ListSkills access class in #2301.

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

Baseline checks

  • Linting (task lint)
  • Fast offline test suite (task test)
  • Pre-review race suite (task test:race)
  • Offline demo (go run ./cmd/mecademo)
  • Markdown changed: docs generation/link checks (task docs)
  • User docs/user-facing behavior changed: site build (task site:build)
  • Guarded engine API affected: compatibility check (task api:check)
  • Intentional engine API change: task api:update + engine/CHANGELOG.md

Studio and docs only, no Go code touched. The Go checks above are not applicable.

  • task studio:check (lint, typecheck, tests, generated-contract check) passes on this branch rebased on main: 219 server and 1,417 web tests.
  • task docs and task site:build pass with the new paragraph in user-docs/operating/studio.md.

Due diligence record

What was checked, how, and what each check can and cannot show. The second commit addresses a review of the first; each fix has a test that I confirmed fails when the fix is removed (a larger concurrency limit, no over-cap skip, per-file failures propagating, an error shown over loaded data, and no early files request).

Check How Result
Static gates task studio:check, task docs, task site:build Pass
Published SDK Swapped the real @stacklok-oss/mecatl-sdk 0.5.0 from npm in for the checked-out link; typechecked and ran the BFF tests Pass. Studio is built from the checkout (#2288), so this shows the code also works against the released package, not how it ships
Real data path Real mecated, real BFF, the repo's own skills, plus synthetic skills for a binary file, an oversize file, a long file name, and a 40-file skill Files, refusals, the 32-shown and 8-omitted cut, and 404 skill_not_found behave as specified
Rate-limited daemon --rate-limit 2 --rate-burst 5 with a skill that needs 7 daemon calls Before the fix the route returned 429 and no files. Now it returns 200 with the files that loaded and a reason on the rest (in the run, three of six files failed, including SKILL.md, because the burst was spent)
Real browser, Chromium Playwright against the Vite app, light and dark, desktop and 430px Highlighting; the instructions label on the first entry only; the fade at the top, middle, and bottom (computed values); refusal rows; a tooltip with the full name on a truncated header and none on a short name; no Manage section; no horizontal overflow; no console or page errors
Real browser, Firefox Playwright with Firefox: no animation-timeline support A block that does not scroll has no fade. Before the fix shadcn's fallback faded it on both edges
Keyboard Tab through the page in Chromium and Firefox Both reach a scrolling block without a tabIndex
Learned skills A real on-disk learned-skill store seeded through the learned-skill repository: two active, one staged, one draft Active ones appear in the All list as agent-owned and open this page with exactly one entry, the instructions. The staged one opens the review dialog. Staged and draft skills are not in the live skill view, so the files route answers 404 for them
Daemon API under it #2192 was reviewed and approved; the review found two bugs (skills unreadable without a learned-skill store, and a NUL-byte file served as text), reproduced through this page, fixed, and re-checked Fixed in #2192

Not tested: a real browser session with sign-in; skills with hundreds of files; the effect of the publication lock under heavy concurrent learned-skill activity (33 reads took about 29 ms with a small store and about 12 ms without one); a learned skill produced by the real learning pipeline instead of a seeded store; Safari (WebKit would not launch here, so the keyboard check covers Chromium and Firefox only); Firefox for anything but the fade fallback.

Changes

File Change
apps/web/src/features/knowledge/skill-files.tsx The Files list: the instructions label, highlighted text per file, refusal rows, the omitted-files line, a tooltip for truncated names, and kept files when a refetch fails
apps/web/src/features/knowledge/configured-skill-detail.tsx Uses SkillFiles and starts its request on open; no Manage section
apps/web/src/styles.css The scroll-fade-y utility, without a fallback
apps/server/src/mecatl/knowledge.ts, routes/knowledge.ts, app.ts The listSkillFiles service (bounded reads, per-file failures), the route, and the skillFiles capability from the skill_files feature
apps/contracts/** The response schema, with instructions, and the regenerated OpenAPI and client
user-docs/operating/studio.md What the Files section shows and its limits
tests Component, BFF service, route, and detail page tests

User-facing change

A configured skill's page lists its files with their text. The first entry is the skill's instructions, without frontmatter.

Special notes for reviewers

Intentional limitations, recorded in the code comments:

  • No paging, virtualization, or load-more: a skill with dozens of files is very unlikely, and showing them well needs a real front-end design that does not exist yet. How large skills should load is tracked in Skills: expose frontmatter metadata to clients and reconcile the skill page with the latest prototypes #2301.
  • A binary file is listed with a message, not offered for download. The daemon refuses non-text reads by design, so a download needs a new byte-level contract first.
  • No fallback for a daemon that predates the skill-file RPCs: the route answers 501 and the panel shows that message. Studio is built from the same commit as the daemon contract it targets.
  • A file that fails to load is not retried. Under a tight daemon rate limit some files show "Couldn't load this file."; reloading the page retries.
  • The scrolling block has no role or tabIndex: Tab already reaches it in Chromium and Firefox, Safari was not tested, and the repo's a11y lint forbids tabIndex on a pre.

Fully or partially written by an AI agent.

🤖 Generated with Claude Code

@kantord
kantord added this pull request to stack #2156 October 7, 2026 14:13
@kantord kantord changed the title feat(studio): add Manage and Files placeholders to the configured skill page feat(studio): Manage and Files placeholders for the configured skill page Oct 7, 2026
@kantord
kantord force-pushed the issue-1853-placeholders branch from 37bba60 to f47dbe8 Compare October 7, 2026 15:33
@kantord
kantord force-pushed the issue-1853-placeholders branch from f47dbe8 to eacdc3d Compare October 7, 2026 15:41
Base automatically changed from issue-1853 to main October 7, 2026 17:33
@kantord
kantord force-pushed the issue-1853-placeholders branch from eacdc3d to 4fb5501 Compare October 7, 2026 17:33
@kantord
kantord removed this pull request from stack #2156 October 8, 2026 12:20
@kantord
kantord force-pushed the issue-1853-placeholders branch from 4fb5501 to 846bc07 Compare October 8, 2026 12:21
@kantord
kantord changed the base branch from main to skill-files-rpc October 8, 2026 12:21
@kantord
kantord added this pull request to stack #2193 October 8, 2026 12:21
@kantord kantord changed the title feat(studio): Manage and Files placeholders for the configured skill page feat(studio): show a skill's files on its page Oct 8, 2026
@kantord
kantord force-pushed the issue-1853-placeholders branch 2 times, most recently from 01922a6 to d4961c1 Compare October 8, 2026 14:44
@kantord
kantord force-pushed the issue-1853-placeholders branch 2 times, most recently from 9f582ba to 5d04990 Compare October 13, 2026 09:13
Base automatically changed from skill-files-rpc to main October 13, 2026 09:38
@kantord
kantord force-pushed the issue-1853-placeholders branch 3 times, most recently from 98e97d8 to 71559a1 Compare October 13, 2026 13:36
@kantord
kantord marked this pull request as ready for review October 13, 2026 13:47

@peppescg peppescg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Daniel. I reviewed this at 71559a1b2, together with the daemon side from #2192.

Before merge

  1. SKILL.md isn't the real file (apps/server/src/mecatl/knowledge.ts:240). The daemon lists the instruction body without its frontmatter under the name SKILL.md, and says "clients key off [SkillFileInfo.instructions], not off the name" (internal/adapter/server/skill_files.go:67-70). The BFF drops instructions, so the page shows the stripped body as SKILL.md, with the body's size. Someone checking allowed-tools or description won't find them there. Carry instructions through the contract and label that entry as the instructions, without frontmatter. The description's "the skill's real files" needs the same caveat.
  2. One page view is 1 + N daemon calls on one lock (knowledge.ts:238-250). When a learned-skill store is configured, every ListSkillFiles / ReadSkillFile starts with beginSkillRead, which takes the caller's skill-publication lock and re-publishes learned skills (skill_files.go:119-133, 5s timeout). So up to 33 calls queue behind one lock, each re-publishing, and they compete with learned-skill activate and rollback. Any error that isn't one of the three skill_file_* codes is rethrown, so Promise.all fails the whole panel: with --rate-limit set (off by default), a skill with more files than the burst gets a 429 instead of its files. The daemon deliberately rejected a single "body plus every asset" read (skill_files.go:33). Load file contents on demand, per file, or at least bound the concurrency, skip reads whose listed size is already over the 25,000-byte cap, and turn per-file failures into unavailable so one failure doesn't hide the rest.
  3. Docs. user-docs/operating/studio.md:114 still says "Select a skill to see its description and slug". Add the limits a reader can't infer from the page: only the first 32 files load, binary or oversize files aren't shown, and SKILL.md is shown without frontmatter. Then run task docs and task site:build.

Small, can go in now

  • Gate on the skill_files feature (apps/server/src/routes/knowledge.ts:361), not on capabilities.skills. internal/adapter/server/features.go:39-43 says a client checks it before offering a skill's files, and the BFF already reads snapshot.features for server_info.
  • Tests. Add /api/v1/skills/{name}/files to the "503 runtime_unavailable on every knowledge route" table (apps/server/src/routes/knowledge.test.ts:577). Restore the toContain("<b>escaped</b> deploys") assertion in configured-skill-detail.test.tsx:78; without it the querySelector("b") check also passes when the description isn't rendered at all.
  • Background refetch errors (skill-files.tsx:46). In TanStack Query v5, isError is also true when a background refetch fails and data is still there, so files already on screen are replaced by the error panel. Keep showing the data when there is some.
  • formatBytes (skill-files.tsx:94) duplicates the one in lib/formatters.ts, and without an MB unit a 5 MB asset reads "4882.8 KB".
  • Firefox fade (styles.css:477). Without animation-timeline, the fallback fades both edges of every block all the time, by up to 40px, so taller blocks that don't scroll lose part of their first and last lines. Fade only when the block overflows, or drop the fade in the fallback.
  • Long names and scrolling (skill-files.tsx:54, :60). A truncated file name has no title, and the scrolling <pre> can't take keyboard focus in Safari; add tabIndex={0} and an aria-label.
  • Manage placeholder (configured-skill-detail.tsx:46, :87-119). 33 lines behind SHOW_MANAGE_PLACEHOLDER = false, plus a test that pins the constant. I'd drop it and bring it back from history with #2184 / #2185.
  • Learned-skill comment (skill-files.tsx:38-40) says a learned skill's page shows no Files section, but an active learned skill opens this page with a one-file Files section.

Can be follow-ups

  • The Files request only starts after the inventory has rendered (configured-skill-detail.tsx:124). Starting it from the route parameter removes the wait on a cold load.
  • The route's OpenAPI responses don't declare 429, 400 or 409, which onError can return.

kantord and others added 2 commits October 14, 2026 13:35
Replace the Files placeholder on the configured skill page with the skill's real files: SKILL.md first, then the bundled files alphabetically, each with its size and syntax-highlighted text in its own scrolling block that fades at the edges. A file that cannot be shown, because it is too large or not text, stays listed with a one-line reason, and files past the first 32 are summarized as "N more files aren't shown".

Add the BFF route GET /api/v1/skills/{name}/files, which loads every file's text in one response so the browser makes a single request, and hide the Manage section behind a default-off SHOW_MANAGE_PLACEHOLDER flag until a write contract exists.

This needs a published @stacklok-oss/mecatl-sdk that contains skills.listFiles and skills.readFile, so it cannot typecheck against 0.4.0. After that release, bump the pin in server/package.json, the age-gate exclusion in pnpm-workspace.yaml, and the lockfile.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review of the Files section found it showed a body without frontmatter as
if it were the real SKILL.md, and that one slow or rejected file read could
take the whole panel down.

- Label the instructions entry from the daemon's `instructions` flag, so it
  reads "Instructions, without frontmatter" and not as the raw file.
- Read at most four files at a time, skip a file already listed as over the
  read cap, and show any other per-file failure on that file only. A rate
  limited daemon failed the whole page before; a rejected session still
  reaches sign-in.
- Gate the route on the `skill_files` feature, not on the skills capability.
- Keep showing files when a background refetch fails, and use the shared
  byte formatter and a tooltip for truncated file names.
- Start the files request when the page opens instead of after the inventory.
- Drop the hidden Manage placeholder and the fade fallback for browsers
  without scroll-driven animations, which faded blocks that do not scroll.
- Document the Files section in the Studio user docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@kantord

kantord commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Thanks @peppescg, this was a thorough review, and the 429 point was real. Everything below is in 6067e6015. The first commit is the one you reviewed, rebased, so the diff since then is the second commit.

Before merge

  1. SKILL.md isn't the real file. Agreed. The BFF now carries instructions through the contract, and the first entry reads "Instructions, without frontmatter". The PR description no longer says "real files". The frontmatter fields themselves (allowed-tools, license) aren't exposed by any client RPC; that's Skills: expose frontmatter metadata to clients and reconcile the skill page with the latest prototypes #2301.
  2. 1+N calls and the 429. I reproduced it first: with --rate-limit 2 --rate-burst 5, a six-file skill returned 429 and no files. Now reads run at most four at a time, a file already listed as over the 25,000-byte cap isn't requested, and any other per-file failure shows "Couldn't load this file." on that file only. A rejected session still propagates, so sign-in works. The same rate-limited run now returns 200 with the files that loaded. In that run three of six failed, including SKILL.md, because the burst was spent, and there is no retry. I also measured the lock: 33 reads took about 29 ms with a learned store and about 12 ms without, so I didn't redesign loading. Per-file on-demand loading is in Skills: expose frontmatter metadata to clients and reconcile the skill page with the latest prototypes #2301, next to the response-shape decision.
  3. Docs. I added a paragraph on the Files section to studio.md. task docs and task site:build pass.

Small

  • The route is gated on the skill_files feature (501 skill_files_unsupported).
  • /files is in the 503 table, and the <b>escaped</b> assertion is restored.
  • A failed background refetch keeps the loaded files. My first test of this passed vacuously, so I rewrote it, and it now fails without the fix.
  • formatBytes uses the shared helper.
  • I dropped the Firefox fade fallback. In real Firefox there's no animation-timeline, and shadcn's fallback faded a block that doesn't scroll on both edges.
  • Truncated names use Tooltip onlyWhenTruncated, the repo's replacement for title=.
  • The learned-skill comment is corrected, and the Manage placeholder is removed.
  • One I did not take: tabIndex and aria-label on the scrolling <pre>. Tab already reaches the block in Chromium and Firefox, which I checked. I couldn't test Safari here, and Biome's a11y rules reject tabIndex on a pre. I noted it in the PR. If you know Safari fails, I'll add it with a suppression and the reason.

Follow-ups

  • The files request now starts when the page opens, with a test.
  • None of the 16 knowledge routes declares 429 or 400 in OpenAPI, so I added one checkbox to Large batch of tiny follow-up fixes #2303 for all of them instead of fixing only this route.

Checks: task studio:check passes after rebasing on main (219 server, 1,417 web tests). I confirmed each new test fails when its fix is removed. I also ran a real-browser pass in Chromium and Firefox with a real daemon and a learned-skill store. The PR description has the full record and what wasn't tested.

Fully or partially written by an AI agent.

@peppescg peppescg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Daniel. Every before-merge item is fixed, and I checked each one against the daemon side, so this is good to merge from my end.

Checked: the limiter is bounded, keeps order, and leaves no unhandled rejections; the > 25_000 skip matches the daemon's own size > skillfs.MaxOutputBytes check exactly; instructions flows from the daemon through the contract and BFF to the web, which keys off the flag; the gate uses the same skill_files identifier the daemon advertises; rethrowing only unauthenticated/authentication matches the other BFF routes; and the refetch test does fail on the old isError branch.

Nits, none blocking:

  • apps/web/src/features/knowledge/skill-files.tsx:92: Safari still doesn't make overflow scrollers keyboard-focusable (Chromium only started in 130, WebKit hasn't shipped it), which is what axe's scrollable-region-focusable rule is about. tabIndex={0} plus aria-label={file.name} with a biome-ignore giving the reason, or a focusable role="region" wrapper, would cover it.
  • skill-files.tsx:73-79: the truncated-name tooltip only opens on hover, because the trigger <span> can't take focus. Same keyboard gap; fine to fix together or in a follow-up.
  • apps/server/src/mecatl/knowledge.ts:569: per-file failures are swallowed without a trace. A debug log with the error code would make a rate-limited daemon showing "Couldn't load this file." diagnosable.
  • knowledge.ts:573-588: after an auth rethrow the other workers keep reading. A stopped flag checked in the while loop would avoid the wasted daemon calls.
  • user-docs/operating/studio.md:120-121: "highlighted by file type, each in its own scrolling block" is visible in the UI, so per user-docs/AGENTS.md the paragraph can start at "The first entry…". Line 117 still says selecting a skill shows "its description and slug"; worth adding "and files".

@peppescg
peppescg merged commit 76762a2 into main Oct 15, 2026
31 checks passed
@peppescg
peppescg deleted the issue-1853-placeholders branch October 15, 2026 09:51
kantord added a commit that referenced this pull request Oct 15, 2026
…ard-coding the route count

main's Studio job has been failing since the skill files route (#2155) landed
next to the knowledge-route convention test (#2313). Each change was fine on
its own: #2313 declared 400 and 429 on the 15 knowledge routes and pinned the
count at 15, and #2155 added a 16th route without them.

- Spread `transportErrors` into `listSkillFilesRoute` like the other knowledge
  routes. The OpenAPI document and generated types gain the two statuses.
- The convention test no longer asserts an exact route count, so the next
  knowledge route is checked without editing the test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 6067e601 Deployed Oct 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio skills: inventory, detail pages, and learned-skill review

2 participants