2026 10 09 show everything design - kubed-io/selenium-flow GitHub Wiki
Status: DESIGNED 2026-10-09 by Claude, unattended; open for Dr K on issue #63.
Epic E7 of the programme spec
(2026-10-09-workspaces-and-observability-design.md, cited as programme
R). Plan: docs/superpowers/plans/2026-10-09-show-everything.md. Assumes
E1 (#60) merged: every URI below is the renamed one. Penpot: file Admin UI,
page App ยท show, boards site-data, site, file, document, and
context (shared with E3); flow App. Read, never edited.
Programme R17 (Dr K, 2026-10-09): every URI the server serves โ every listed
resource and every template โ has a view in show's table, and a test holds
it. A single file is drawn from its listing entry, never its bytes; markdown
and the flow schema are drawn as a document; MAX_SHOWN still applies. The
issue adds: a screenshot as an image, a recording as a player, a download or
kept file as a card with Open; site-data and site-data/{site} as a
drill-down pair with httpOnly values โขโขโข; the files and folder views
drill into one file; the skill's show line and the wiki note. Out: E3's
capture views, fullscreen for the player.
Left open by the issue: a screenshot from its signed link or inline; what a document does with a relative link.
Checked against the branch on 2026-10-09, with probes run outside the worktree.
The inventory, enumerated for real. A FastMCP Client against
SeleniumMCP with data.dir, secrets.dirs and recording.enabled set, and
again with nothing set: the listing is the same either way (a section that is
off still lists its resource and refuses the read). With the UI built, one more
resource appears, the app shell.
| # | URI form | Listed as | MIME | View today |
|---|---|---|---|---|
| 1 | workspace://current |
resource | json | context |
| 2 | workspace://site-data |
resource | json | โ |
| 3 | workspace://site-data/{site} |
template | json | โ |
| 4 | workspace://files |
resource | json | files |
| 5 | workspace://files/screenshots |
resource | json | folder |
| 6 | workspace://files/recordings |
resource | json | folder |
| 7 | workspace://files/downloads |
resource | json | folder |
| 8 | workspace://files/{name} |
template | octet-stream (json for a video) | โ |
| 9 | workspace://files/screenshots/{name} |
template | octet-stream | โ |
| 10 | workspace://files/recordings/{name} |
template | json (the entry) | โ |
| 11 | workspace://files/downloads/{name} |
template | octet-stream | โ |
| 12 | flow://flows |
resource | json | flows |
| 13 | flow://flows/{name} |
template | json | flow |
| 14 | flow://schema |
resource | json | โ |
| 15 | secret://secrets |
resource | json | secrets |
| 16 | skill://selenium-flow/SKILL.md |
resource | markdown | โ |
| 17 | skill://selenium-flow/_manifest |
resource | json | โ |
| 18โ25 | skill://selenium-flow/references/{CONFIGURATION,FLOWS,INTERACTION,READING_PAGES,SECRETS,SITE_DATA,TROUBLESHOOTING,WORKSPACES}.md |
resources | markdown | โ |
| 26 | ui://selenium-flow/component |
resource, only when the UI is built | text/html;profile=mcp-app |
the shell itself |
So 19 listed resources and 6 templates (20 listed with the UI built). Eight
forms have a view; 17 do not: site-data, site-data/{site}, the four
single-file templates, flow://schema, and the ten skill files.
Sizes, against MAX_SHOWN (100 000). The largest document is
references/FLOWS.md at 15 080 characters (15 812 as a payload);
flow://schema is 11 477; _manifest 1 504. Every document fits with room.
A listing entry is a few hundred characters. A screenshot's bytes would not:
base64 adds a third, and a viewport PNG of a real page is commonly over 100 KB.
A recording is megabytes.
What read_resource hands show. A skill file reads as one
ResourceContent whose content is a str and whose mime_type is
text/markdown; show today json.loads it and refuses. The item templates
for screenshots, downloads and Files return bytes; recordings/{name}, and
files/{name} for a video, return the listing entry as JSON (http/files.py,
_item_resource, recording_resource).
Where an entry comes from. Every folder's listing already describes each
file once, with its name, uri, size, created, content_type, image,
a signed url and, outside Files, keep_with (files.describe). A listing
never opens a browser (Workspaces.browser, never resolve); the Downloads
listing asks the Grid's /se/files store for a live browser and nothing for an
absent one.
The skill names its files by URI, not by relative link. Across SKILL.md
and the eight references there are two markdown links, both https://โฆ, and
one ](url) inside a code example. Every pointer to another file is a code
span holding an absolute URI (`skill://selenium-flow/references/FLOWS.md`,
`workspace://files`), as AGENTS.md requires. Some are placeholders
(`flow://flows/{name}`, `workspace://`).
The markdown the skill uses. marked's lexer over all nine files, with
SKILL.md's YAML front matter removed, yields block tokens heading,
paragraph, list, code, table, space and inline text, strong,
em, codespan, link. Left in, the front matter lexes as a rule and a
setext heading. Each file opens with one # heading.
The bundle. The app is 84.4 KB gzipped against a 110 KB budget
(npm run size, this branch). A vite 8 minified ES build of marked 18.1.0
measured 15.0 KB gzipped with its renderer, 14.6 KB importing only Lexer;
markdown-it 15.0.2 measured 42.6 KB, which would not fit.
Escaping is enforced. ui/eslint.config.js makes svelte/no-at-html-tags
an error and forbids innerHTML ("Render with a template"). A markdown
library's HTML string cannot be dropped into the page; tokens have to be drawn
by Svelte.
Relative URLs resolve on our schemes. WHATWG URL treats skill: as a
non-special scheme with a host and a path: new URL('references/FLOWS.md', 'skill://selenium-flow/SKILL.md') is skill://selenium-flow/references/FLOWS.md,
and '../SKILL.md' from a reference climbs back (Node 24, measured).
What the host lets the app reach. ResourceCSP.resource_domains maps to
img-src and media-src as well as scripts, styles and fonts (ext-apps 2.0.3,
spec.types.d.ts), so the server's own origin โ declared when
PUBLIC_BASE_URL is set (apps._csp) โ covers an <img> and a <video>.
The host advertises openLinks when it can open a URL for the app
(App.openLink).
The boards. site-data: a card titled Site data with its URI in mono
beside it and saved 12:01 at the right; one row per host with a counts pill
and a chevron; a row navigates to site. site: โ Site data, the host with
its URI, a Cookies block (name, value or โขโขโข, then httpOnly ยท secure ยท
session or .herokuapp.com ยท 1 y), a Local storage ยท block of
key/value lines. file: a recording โ name, URI, โ REC, a 16:9 player, a
facts line 4:12 ยท 18.4 MB ยท 21:12 ยท firefox, an Open button. document:
TROUBLESHOOTING.md rendered โ its # heading as the title beside the URI,
paragraphs, a ## heading, a code block. context: chips Console, Network
(E3's), Site data 2 and Files 111; the Site data chip navigates to
site-data, the Files chip to nothing.
Claude, 2026-10-09, unattended โ each for Dr K to overturn on #63:
-
A single file is drawn from its folder's listing entry, looked up by
show. For a single-file URI,showreads the folder's own listing resource (workspace://files, orโฆ/screenshots|recordings|downloads) through the server and takes the entry whosenameis the URI's last segment, unquoted. No new resource; the item templates keep serving bytes, which is what lets a client that reads resources display a screenshot with no URL (http/files.pydocstring). Cost if wrong: one listing read per file shown โ for a download, the same Grid/se/filescall the Downloads view already makes; if a folder ever grows large enough for that to hurt, JSON item resources are the fallback. -
A screenshot is drawn from its signed link, never inline. A
real screenshot's base64 alone passes
MAX_SHOWN; the entry is a few hundred characters. Cost if wrong: withoutPUBLIC_BASE_URLthe host's CSP blocks the image and the view says so (Design 8); a chat reopened afterlink_ttl(default 3 600 s) shows the same line. -
Markdown is lexed by
marked18.1.0 and drawn by Svelte components; never as an HTML string. OnlyLexeris imported. Raw HTML in a document is drawn as its text. A token type the renderer does not know is drawn as itsrawtext. Cost if wrong: 14.6 KB of the remaining 25.6 KB of budget; a construct the skill starts using renders as plain text until the renderer learns it. -
Links in a document. A link, or a code span whose whole text is a
concrete URI on one of the server's schemes (
skill,flow,workspace,secret, with no{ } < >or space), drills throughshow. A link'shrefresolves against the document's URI first, so a relative link to a sibling file works though none exists today.http(s)links open through the host'sopenLinkwhen it offersopenLinks, else as a plain link in a new tab. Anything else โ#anchors,javascript:, placeholders โ is text. Cost if wrong: a URI with no resource behind it lands asshow's refusal in the shell's error line. -
A document's
datais the resource's own content: its text fortext/markdown, its JSON when it parses, and any other UTF-8 text as one fenced code block. Thedocumentview draws a string as markdown and anything else as a JSON tree, so the fence is what keeps a YAML comment from becoming a heading. Front matter is not drawn; the first#heading becomes the title. Bytes that are not UTF-8 are refused. Amended at review: a supporting file the skill ships that is not markdown reads back as bytes under its own type (application/yaml), matched the skill's row, and was refused โ the guard only proved the URI routed. -
Inline, a document shows its first six blocks and "+N more" when the host
offers fullscreen; everything when it does not. Claude's guidelines keep
inline views short and unscrolled, and a host with no fullscreen would
otherwise hide the rest for good. The shell tells views whether it can
expand (
expandable). Cost if wrong: a tall inline document on a host without fullscreen. -
Component names:
file,sites,site,document.sites/siteare the plural/singular pair besideflows/flow; the board is calledsite-data. Cost if wrong: a rename. -
ui://is exempt from the guard, by scheme. The app shell is the drawing, not a thing to draw. Cost if wrong: none until a secondui://resource that is not a shell. -
The guard enumerates through a real client on a server with every section
on โ data, secrets, recording, the built UI โ instantiates each template
by putting
samplefor each{variable}, and assertsview_formatches. A second assertion holds the other end: every componentshowcan name is a key of the shell'sVIEWSinApp.svelte. Cost if wrong: a resource registered only under a setting the guard's server leaves off escapes it โ so the fixture turns every section on, and its docstring says why. -
The
filesandfoldertiles drill intofilewhen the host can call tools; the Lightbox stays for a host that cannot. Cost if wrong: the drill path loses the Lightbox's Prev/Next. -
Open uses the host's
openLinkwhen it offersopenLinks, else a plain link (target="_blank" rel="noopener"). A host that refuses (isError, e.g. a blocked domain) or fails puts "The host would not open this link: " in the shell's error line, since the view has already cancelled the anchor. Cost if wrong: on a host with neither, Open does nothing; the URL is still in the entry the model holds. -
Back names the view it returns to (โ Site data, as drawn), from the
component below it on the stack:
filesFiles,folderits folder,flowsFlows,sitesSite data,contextWorkspace,secretsSecrets,documentits file name; anything else Back. Its accessible name stays Back. Cost if wrong: one label table in the shell. -
The context view gains the Site data chip; the Files chip waits. The
board wires only Site data, and
workspace://currentcarries no files count to put on a Files chip. Cost if wrong: the drawn Files 111 chip is missing until the resource carries a count. - The file view's facts are what the entry carries, plus the duration a video reports once loaded. The drawn firefox is not in the entry (a recording's note keeps only the Grid id), so it is left out. Cost if wrong: one fact fewer than drawn.
-
show's description names the schemes, not sixteen forms. "Draw any resource this server serves โ workspace://, flow://, secret:// or skill:// โ for the person to see"; the refusal still names every form. Cost if wrong: a model that guesses a URI is told the forms on its first miss. -
The
siteview is read-only. No Forget, no Clear: the show spec keeps editing out of the app. The admin pane's counting rule moves toui/src/lib/sites.tsso both surfaces count a host alike; markup is not shared, because the board and the admin pane lay a site out differently. Cost if wrong: two small views of one shape to keep in step. - The Penpot file is Admin UI. The programme spec calls it selenium-flow; the issue and the connected file say Admin UI.
show draws all 25 URI forms the server serves today: one file as a picture,
a player or a card with Open; the saved site data as a list that opens a site;
the skill's pages rendered and linked; flow://schema and _manifest as a JSON
tree. A test fails the build the moment a resource or template is added
without a view.
| Not built | Why |
|---|---|
console, network, request views |
E3's, under the same guard |
| Fullscreen for the player or the Lightbox | the show spec's next round |
| Forget, Clear, keep, delete from the app | the show spec: no editing in the app |
| A count on the Files chip | needs workspace://current to carry one (Ruling 13) |
| Refreshing an expired link in place | next round |
| JSON item resources for single files | Ruling 1 |
readServerResource |
still unconfirmed on Claude |
These hold for every task in the plan.
-
MAX_SHOWNstays 100 000 characters, payload and line as sent. - The URI-to-component table lives in
kubed/selenium_flow/mcp/show.pyand nowhere else. - A single file is drawn from its listing entry, never its bytes; no resource is added or changed.
-
shownever callsWorkspaces.resolve: drawing anything never opens a browser. - The app stays within 110 KB gzipped (
npm --prefix ui run size). -
markedis pinned at exactly18.1.0independencies, and onlyLexeris imported from it. - No
{@html}and noinnerHTML(the eslint config already errors on both). - A link is clickable only when
safeHrefpasses it or it is a concrete URI onskill,flow,workspaceorsecret. - httpOnly cookie values arrive as
โขโขโขfrom the server and are drawn as given. - Python 3.10โ3.14,
from __future__ import annotations;ruff check kubedclean. - UI:
npm --prefix ui run check,lintandtestclean. - One CHANGELOG line, in
[Unreleased]only. - Penpot is read, never edited.
VIEWS in mcp/show.py becomes rows of a View named tuple โ form,
pattern, component, and entry (true when the URI is drawn from its
listing entry). First match wins, so each folder sits before the
{name} row it would otherwise match.
| Form | Pattern (fullmatch) | Component | Entry |
|---|---|---|---|
workspace://current |
literal | context |
|
workspace://site-data |
literal | sites |
|
workspace://site-data/{site} |
workspace://site-data/[^/]+ |
site |
|
workspace://files |
literal | files |
|
workspace://files/screenshots |
literal | folder |
|
workspace://files/recordings |
literal | folder |
|
workspace://files/downloads |
literal | folder |
|
workspace://files/screenshots/{name} |
โฆ/screenshots/[^/]+ |
file |
โ |
workspace://files/recordings/{name} |
โฆ/recordings/[^/]+ |
file |
โ |
workspace://files/downloads/{name} |
โฆ/downloads/[^/]+ |
file |
โ |
workspace://files/{name} |
workspace://files/[^/]+ |
file |
โ |
flow://flows |
literal | flows |
|
flow://flows/{name} |
flow://flows/[^/]+ |
flow |
|
flow://schema |
literal | document |
|
secret://secrets |
literal | secrets |
|
skill://selenium-flow/{path} |
skill://selenium-flow/.+ |
document |
view_for(uri) -> str keeps its signature (other modules and tests call it);
row_for(uri) -> View is new and is what show uses. SHOWABLE is still the
forms in order, and still what the refusal names.
-
A whole resource (
entryfalse): read through the server as today. If the content isbytes, it is decoded as UTF-8, refused when it is not ("<uri> is not a resource show can draw"). Text withmime_typetext/markdownisdataas it is; otherwisedataisjson.loadsof it, or, when it is not JSON, the text as one fenced code block (Ruling 5). -
One file (
entrytrue): the listing URI is everything before the last/;showreads that listing the same way, unquotes the last segment, and takes the entry infileswhosenameequals it. None:ValueError("no file at <uri>. <listing> lists what there is"). The result'suriis the URI asked for;datais the entry. - The payload, the one-line summary and the
MAX_SHOWNcheck are unchanged.summaryalready falls back to(<component>)for a payload with nocount, which is every new view.
All live in ui/src/lib/views/, take the host's tokens, and carry the
resource URI in mono beside the title as the boards draw it (the shell passes
uri; it is shown decoded, as a person reads it).
-
FileView(file): the name, the URI,โ RECwhen the URI is a recording. Then a preview bycontent_type:imageโ<img>at full width, height capped;video/*โ<video controls preload="metadata">, 16:9; PDF โ an<iframe>as the Lightbox does; anything else โ the glyph block. A facts line โ the video's duration onceloadedmetadatafires,bytes(size), the created time (clock time today, the date otherwise), and for a non-media file itscontent_typeโ and Open at the right (Ruling 11). When the image or video fails to load, the preview is replaced by one line: "This link has expired, or this server's address is not reachable from here (PUBLIC_BASE_URL)." -
SitesView(sites): Site data, the URI, saved whensaved_atis set. One row persitesentry: the host,siteCounts(row)as a pill, a chevron; the row is a button callingonshow(row.uri). Withoutonshowthe rows are text with the "Open isn't available in this client" tooltipFilesViewalready uses. Empty: "Nothing saved โ an agent calls save_site_data after signing in." -
SiteView(site): the host and the URI. Cookies: one line each โ name, value (clipped, whole on hover), then facts joined byยท:httpOnly,secure, the domain when the cookie isshared, andsessionor the time left (until(expiry): 45 m, 5 h, 3 d, 1 y). Then per origin, only where it has entries, Local storage ยท and Session storage ยท as key/value lines. A host with nothing at all: "Nothing saved for this site." -
DocumentView(document): a string is markdown โ front matter removed, lexed, the first#heading lifted into the title, the rest drawn byMarkdown.svelte(block tokens) andMarkdownInline.svelte(inline tokens). Anything else is drawn byJsonTree.svelte, titled by itstitlewhen it has a string one, else the URI's last segment. Inline limit per Ruling 6 (blocks counted withoutspace). -
JsonTree: an object or array is a<details>per key with a count in its summary, open at the top level; scalars askey: value, strings quoted.
Rendering is tokens to elements, one {#if} per type: heading (h1โh6, the
app's own scale), paragraph, text (its inline tokens when it has them),
list (ordered or not, items' tokens), code (<pre><code>), table
(header and rows of inline tokens), blockquote, hr, space (nothing);
inline strong, em, codespan, link, del, br, escape, text.
Anything else, and html, is its raw as text.
ui/src/lib/links.ts decides what a link is: target(href, base) โ {kind: 'show', uri} | {kind: 'open', url} | null and showable(text) โ string | null
for a code span (Ruling 4).
All through the shell's onshow, which calls show with callServerTool
and pushes the result โ nothing new in the mechanism.
| From | Click | To |
|---|---|---|
files, folder
|
a tile |
file (the tile's uri) |
sites |
a row |
site (the row's uri) |
context |
the Site data chip |
sites (site_data.uri) |
document |
a URI code span or link | whatever that URI draws |
FileGrid already takes onopen(i); FilesView and FolderView pass
(i) => onshow(files[i].uri) when onshow is set and leave it unset
otherwise, which keeps the Lightbox.
App.svelte registers file, sites, site, document; adds document to
EXPANDS; passes every view uri, expandable (the host lists
fullscreen) and onlink (host.openLink({url}) when the host's
capabilities include openLinks, else undefined); labels Back by the view
below (Ruling 12).
tests/test_show_inventory.py:
-
Every listed resource and every template has a view. A server with
data.dir,secrets.dirs,recording(with an inbox) and the built UI; aClientlists resources and templates; each URI, and each template withre.sub(r"\{[^}]+\}", "sample", t), must passview_for, exceptui://(Ruling 8). The failure names every URI without a view. -
The inventory is what this spec says. The same enumeration, with every
skill://selenium-flow/โฆURI folded into one, equals the 16 forms of Research's table, so a new resource fails here as well as above and this table is updated with it; a new skill reference does not. -
Every component
shownames, the shell draws. The set of components inVIEWSequals the keys ofVIEWSinui/src/App.svelte, read by a regex. -
Each form draws what it should.
showthrough the client returns the expected component for one concrete URI of each form (a screenshot, a recording, a download from a faked Grid listing, a kept file, a site with saved data, a skill reference, the schema).
marked adds about 14.6 KB gzipped (measured), the four views and three
renderer components a few more: the app goes from 84.4 KB to roughly 100 KB of
110. The plan runs npm --prefix ui run size in the task that adds marked
and again at the end; over budget stops the task.
| Case | What the person or model sees |
|---|---|
| A URI no row matches | the refusal naming every form, as today |
| A file the listing does not have (gone, a download with no browser) | no file at <uri>. <listing> lists what there is |
| A site with no saved data | the resource's own refusal (no saved data for โฆ workspace://site-data lists them) |
Over MAX_SHOWN
|
as today: read the resource instead |
| An image or video that will not load | the expired-or-unreachable line in place of the preview |
| A markdown construct the renderer does not know | its raw text |
| A drill-down to a URI with nothing behind it | the refusal in the shell's error line; the view stays |
-
SKILL.md's tool table:showโ "draw any resource for the person (hosts that render apps): the workspace, files and one file, site data, flows, the secrets, these pages, the flow schema". - AGENTS.md, "
showis the one app tool": every URI the server serves has a view,tests/test_show_inventory.pyholds it; a single file is drawn from its listing entry; documents are lexed, never{@html}. - README: the apps paragraph lists the new views.
- CHANGELOG
[Unreleased]: "showdraws every resource: one file, the saved site data, the skill's pages and the flow schema." - Wiki:
Installing.md(and any pagegrep 'show('finds) names the views; generated pages are untouched, sinceshowhas no endpoint.
-
Python (
tests/test_show.py,tests/test_show_inventory.py): the guard (Design 6); a single file from its entry for each folder, itsurlsigned and no bytes read; a missing file refused naming the listing; a skill file'sdatais its text;flow://schemaand_manifestare their JSON; the site pair, with an httpOnly valueโขโขโขin what is sent; no browser opened by any of it; the description names the schemes; the refusal names every form. -
UI (vitest + testing-library):
links.tscases;Markdownper token type, front matter dropped, raw HTML as text, a URI code span as a button that callsonshow, anhttpslink throughonlink;JsonTree; each view and its empty or failed state; tiles and rows drilling; the context chip; the shell registering the four components,expandable,onlinkand the Back label;siteCountsunchanged for the admin pane. - Live, Dr K on Claude web: "show me my last screenshot", "show me the recording", "show me the saved site data" โ a site, "show me the skill" โ a reference, "show me the flow schema"; light and dark.
-
The inventory โ 19 listed + 6 templates (+1
ui://with the UI built), 17 forms without a view. Measured (probe, 2026-10-09). -
marked's size and token set โ 14.6 KB gzipped forLexer; the skill uses only the types in Research. Measured. The plan re-measures the real bundle. -
Relative resolution on
skill:โ measured in Node; the plan'slinks.tstest proves it under jsdom. -
A skill file reads as
str+text/markdownthroughfastmcp.read_resource. Measured. -
A
<video>from the server's origin plays inside Claude's app iframe withresource_domainsalone. Unverified โ Dr K's live check; if it does not, the file view's Open is the way to watch. -
Claude advertises
openLinks. Unverified; without it Open is a plain link (Ruling 11). -
A PDF draws inside Claude's app frame. Unverified โ Dr K's live
check. The app is already in the host's sandboxed iframe, and Chrome's PDF
viewer does not load under a sandbox (AGENTS.md, why a PDF link gets no
sandbox), so the file view's<iframe>may stay blank. If it does, a PDF takes the glyph card with Open like any other non-media file.
- A files count on
workspace://current, and the context view's Files chip. - Refreshing an expired link in place (re-calling
showfrom the view). - Fullscreen for the player and the Lightbox.
- Prev/Next inside the
fileview, from the listing it was opened from. - E3's
console,network,requestunder the same guard.