2026 10 04 show design - kubed-io/selenium-flow GitHub Wiki
Status: BUILT on branch show (PR #56), 2026-10-05; final review fixes in; live
test pending. Plan: docs/superpowers/plans/2026-10-04-show.md. Dr K reads this
as it grows and drops rulings in chat; every ruling is recorded under Rulings.
Dr K, 2026-10-04, after selenium-flow went behind the gateway and worked from Claude web: "I want a mcp-app for one flow. I want to ask show me a flow and see it as an mcp app now too. Also let's give a context mcp app." And: can it be one dynamic UI, keyed by which resource is shown, rather than a tool per view?
What exists. The server already serves one app shell,
ui://selenium-flow/component (mcp/apps.py, built from ui/src/App.svelte). It
draws whichever Svelte component a tool result names in component. One tool
uses it: session_files, which returns all three file sections as
component: "fileSections". App tools are listed only for a client that declares
the MCP Apps extension (apps.supported()), and the gateway passes that
declaration through β checked live on both routes, 2026-10-04.
What an app may do (ext-apps 2.0, spec 2026-01-26): call server tools
(callServerTool), read server resources (readServerResource), update the
model's context (updateModelContext), ask for a display mode
(requestDisplayMode), and send a chat message (sendMessage). A tool marked
visibility: ["app"] is hidden from the model; ["model", "app"] is callable by
both.
What Claude actually does (research report, 2026-10-04):
| App request | claude.ai |
|---|---|
callServerTool |
works; the result goes to the app only, the model never sees it |
requestDisplayMode |
inline and fullscreen; pip is refused silently |
updateModelContext |
stored; the model reads it only if it chooses to |
sendMessage |
drafts text into the input box; the user must press send. Changed behaviour once (July 2026) β not to be relied on |
readServerResource |
unconfirmed |
Claude's design guidelines for apps in chat: inline views fit their content
and never scroll vertically inside; carousels of 3β8 equal cards with the next
one peeking; at most one button per card; detail views go fullscreen with the
app's own button; host style tokens only (light and dark); skeletons, not
spinners; no dropdowns or popovers. Selecting and expanding happen in the app;
anything needing Claude happens in chat. claude.ai ignores size-changed and
reads the document's own height, so the app sets documentElement.style.height
after each render. Tool results over ~150k characters never reach the app.
Dr K, 2026-10-04:
-
One
show(uri)tool, keyed by resource URI β not a tool per view, and not an app hung onread_resource(which is the model's reading tool, hidden from clients that read resources natively). -
showreplacessession_files. One user, no back-compat. -
showfollows the resource URIs one-to-one. Asking for screenshots shows screenshots (the lightbox scroller), not the three-section files view. Files and downloads look the same way. - The flow list is a horizontal card scroller, not the admin's list. Selecting a flow shows that flow.
-
The context view is simple:
session://currentβ session name, browser, current URL, live or idle, window, who is signed in. -
No
appsclient flag. The client's MCP Apps declaration already decides, and it crosses the gateway.
show(uri) draws any of six resources as an MCP App, inline in the chat, in one
generic shell. A click on a flow card opens that flow inside the same app.
- No Run button (recommended for a later PR: it needs a parameter form; open for Dr K to override).
- No editing (flows, files, site data) from the app.
- No
sendMessage-driven navigation; nopip. - No change to resources, the mirror tools, or the admin UI's own panes.
show(uri: str) lives in kubed/selenium_flow/mcp/show.py.
- Showable URIs and their views (one table, the single source of truth; matched in order, first match wins):
| URI | component |
|---|---|
session://current |
context |
session://files |
files |
session://files/screenshots |
folder |
session://files/downloads |
folder |
flow://flows |
flows |
flow://flows/{name} |
flow |
-
It reads the resource itself, through the server (
read_resource), so a view draws exactly what the resource serves and cannot drift from it. The result is{"component": <view>, "uri": <uri>, "data": <the resource's JSON>}, as structured content only; the text content is one line for the model ("Showing flow://flows/login to the person (flow)."). Nothing new is computed for the views. -
Too large is refused: a result over
MAX_SHOWN(100k characters, payload and line as sent) says to read the resource instead β Claude drops one over ~150k. -
Anything else is refused with one sentence naming every showable URI
(
ValueError, the house refusal). A URI that names a missing flow refuses the way the resource does. -
App declaration: the shared
apps.config_for(base)gainsvisibility=["model", "app"], so the app can callshowitself for drill-down. It is listed only for a client that renders apps, assession_fileswas; elsewhere it still answers with the same JSON. -
Description (for the model): what it does, the six URIs, that it is for a
person to see (read with resources or
read_resourceto think), and that the view the person is looking at is shared as context.
session_files (http/files.py, FILES_TOOL) and its sections() helper's MCP
use are removed; sections() stays if the admin page still uses it.
ui/src/App.svelte stays the one shell. It gains:
-
A view table keyed by
component:context,files,folder,flows,flow(andfileSectionsgoes withsession_files). -
A small navigation stack. A view may ask to show another URI; the shell
calls
callServerTool({name: "show", arguments: {uri}}), pushes the result, and shows a Back control while the stack is deeper than one. One level is the intended depth (flow list β flow; files β a folder); deeper works but is not designed for. -
updateModelContextafter every view change, with one line:Showing <uri>(+ the flow's name or the folder and count). Best effort; a host without it is ignored. -
Host theming: apply the host's theme and style variables
(
applyDocumentTheme,applyHostStyleVariables,applyHostFonts) from the initial host context and ononhostcontextchanged; keep the currentprefers-color-schemefallback for hosts that send none. -
Sizing:
autoResizestays on, and after each render the shell also setsdocument.documentElement.style.heightto the content's height (the claude.ai workaround). No100vh. -
Fullscreen: the
flowview shows a button only when the host listsfullscreeninavailableDisplayModes(the lightbox is next round: it needs a viewβshell callback). -
Capabilities are checked, never assumed: without
serverTools, a card is not clickable and says so in its tooltip; the view still draws.
All reuse the existing pieces where they exist and the host's tokens for colour.
-
contextβ one card fromsession://current: session name, browser mark (asSessionSummarydraws it), live/idle pill, current URL as a link, window, principal (kind, username), saved-site count. Inline, no scrolling. -
folderβ oneFileGrid, one horizontal row of tiles, for the folder'sfiles, its tiles opening the existingLightbox. Title fromdata.folder, count pill. Screenshots and downloads. -
filesβ the sameFileGrid, one horizontal row of tiles, for Files' own kept files, plus a chip per folder fromdata.folders(Screenshots 4,Downloads 1); a chip drills intoshow(<folder uri>). -
flowsβ a horizontal card scroller fromdata.flows: equal cards with the flow's name, description (two lines, clamped), parameter and step counts, and asharedmark; the next card peeks;scroll-padding-inlinefrom the host's safe area. A card click drills intoshow("flow://flows/<name>"). Empty: one line, no cards. -
flowβ the flow's name and description, its parameters (name, type, default, required) and its steps (number, tool, a one-line summary of the arguments,id/notewhen present,onError: continuemarked). Inline it shows the parameters and the first steps with a "+N more" line; the fullscreen button shows everything.
-
Server (pytest, through the real MCP surface):
showreturns the rightcomponentand the resource's own JSON for each of the six URIs (a flow store and a kept file in fixtures); an unshowable URI is refused naming the six; a missing flow is refused;showis listed only for an app client and carriesvisibility: ["model", "app"];session_filesis gone. The tools golden is regenerated. -
UI (vitest + testing-library): the shell dispatches each component; a card
click calls
callServerToolwithshowand the flow's URI, pushes the view and shows Back; Back pops;updateModelContextis called with the shown URI; noserverToolsβ cards not clickable; the fullscreen button appears only when the host offers it; each view renders its empty state. The app stays within its gzip budget (npm run size, 110 KB). - Live, on Claude web (Dr K): "show me my screenshots", "show me my flows" β click one, "show me the session", in light and dark.
- Skill:
SKILL.md's tool table gainsshowand loses nothing else (session_fileswas not in it); a line inFLOWS.mdandSESSIONS.md. - README: one feature line (advertise).
- AGENTS.md: the apps section β one tool, the view table, drill-down through
callServerTool, why notread_resource, why notsendMessage. - CHANGELOG:
show(uri)draws a resource as an MCP App;session_filesis gone. - wiki: regenerated;
Installing.md's prose namesshowinstead ofsession_files.
- A Run button on the flow view (a parameter form;
run_flowthroughcallServerTool; the result drawn in place and pushed withupdateModelContext). - Fullscreen for the lightbox (a callback from
FolderViewto the shell). -
readServerResourcefor full-size images once Claude confirms it. - More views:
session://site-data,secret://secrets.