Skip to content

feat(clients): app cards and brand-colored activity rows for connector apps - #71

Draft
kvnloo wants to merge 11 commits into
experiment/openui-chart-catalogfrom
experiment/openui-app-cards
Draft

kvnloo wants to merge 11 commits into
experiment/openui-chart-catalogfrom
experiment/openui-app-cards

Conversation

@kvnloo

@kvnloo kvnloo commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Downstream experiment, stacked on experiment/openui-chart-catalog (the openui-v2 chart boards). Tracking: #57 (generative UI track). The phone-side "let T3 use my other apps" idea is a separate RFC: #70.

What you'll see

Launch film v4, shot inside T3 Code

Launch film v4 MP4 with music (1080p60, 69 s). Shot inside the real T3 Code app: apps/web/test/appCardsFilm.renders.tsx mounts AppRoot on the test environment and saves one frame per tick of a virtual clock, with demo connector data. The Android scene is a screen recording of the native cards on a Galaxy S25. Music: "November Snow", The Cynic Project / cynicmusic.com (CC0). Map data © OpenStreetMap contributors, route by OSRM.

A real model turn on Android. Recording: the T3 Code Android app on an S25 sends the prompt to a T3 server, Hermes runs the four demo connectors on a free model (Nemotron Ultra, kilo-free), and the board renders as native cards in about 40 s. Every value on the cards matches what the connectors returned.

Phone width in real Chrome. S25 Ultra emulation (412 CSS px, 3.5x) on a headless display: all eight cards in a replayed thread report 0 px of sideways overflow.

Earlier films (v3 motion studies, v2, raw recording)

Launch film v3: Gmail scene, then the GitHub merge-conflicts sequence

Launch film MP4 with music (1080p60, 71 s). Every scene is an HTML page rendered frame by frame at 4K, not a screen recording. The Gmail, Uber and Maps scenes are motion studies of these cards with demo data (the tilted map is a concept; the PR ships a route line). The GitHub scene recreates @pitus_'s "Generative UI #2 – Merge conflicts"; the cards in this PR are read-only and don't resolve or merge. Music: "November Snow", The Cynic Project / cynicmusic.com (CC0).

The cards as this PR renders them (browser test, demo data):

app cards

Earlier film and the unedited recording of a real turn (cards before commit 8)

40-second film v2

Film v2 MP4 (40 s) · the "before" is a mock; the rest is the real turn below at a labeled 3× and 1.5×.

the raw recording

Raw recording MP4 (40 s) · typing and the model's wait run at 3× and say so; the board and taps are real time.

Media lives on media/openui-app-cards so this diff stays code only.

Running row, over one turn Finished board
running row board

Problem

When an agent uses one of the person's apps, T3 shows a generic row ("Used editor gmail 63ea3519, editor uber 46f286a1, …") and a wall of Markdown. You can't tell at a glance which app was touched or what came back.

What changed (8 commits, read in order)

  1. feat(clients): brand-colored activity rows — tool calls from known apps (Gmail, Google Calendar, Google Maps, Uber, Lyft, GitHub, Slack, Spotify, DoorDash) show the app's colored monogram and a sentence: "Searched Gmail · invoices from Acme", "Checked Uber prices". Grouped rows name the apps: "Used Gmail, Uber, Google Maps, and GitHub". Matching is by MCP server name, so every provider gets it; it also undoes Hermes's editor_<name>_<hex> rename. Shared resolver in packages/client-runtime/src/appConnectors.ts; web row in TimelineWorkEntryRow.tsx, mobile row in thread-work-log.tsx.
  2. feat(clients): OpenUI app cards — four read-only cards for openui-v2 boards: RideOptions (Uber/Lyft, cheapest tagged), MailList (Gmail), Route (Google Maps), PullRequest (GitHub progress track, conflict count, check chips; a merged PR fills its track once via @starting-style / Reanimated entering, reduced motion respected, no continuous animation). Rules live once in openUiAppCards.ts; web and Android bind them to their own zod/OpenUI copies, like the charts. The inspector rejects incoherent cards (conflict state with 0 conflicts, "ready" with a failing check, duplicate ride options).
  3. test(openui-lab): demo connectors — experiments/openui-lab/dogfood/: canned MCP connectors and an ACP proxy that adds them to a sandbox provider instance. Nothing is fetched, booked or merged.
  4. docs(user) — docs/user/openui-reports.md: the cards, the activity rows, and design credits.
  5. fix(clients): use the app sentence in live and single-call work labels — found by dogfooding: the running header and single-call rows resolved their labels on a separate path and still read "Get pull request". All three label paths on web and mobile now use the shared app label.
  6. fix(web): no list markers inside mail and route cards — found by dogfooding: inside chat Markdown the Route card read "3. (3)".
  7. feat(web): brand mark on the live activity row — the running row draws the app's monogram while the call is in flight.
  8. feat(web): richer app cards with commit graph, app links and selection — the cards now read like the apps they come from: RideOptions selects the cheapest row and its Request button opens the ride app; MailList has sender avatars, attachments and "Open in Gmail"; Route steps through one direction at a time over a route line with "Open in Maps"; PullRequest draws recent commits on a small commit graph with conflicting commits marked, after @pitus_. New fields (arrival, attachment, commits) are optional, so existing boards still parse; Android accepts them and keeps its simpler layout. Still monogram tiles, no bundled logos.

Cards stay read-only on purpose: taps only change local selection. Acting through apps (booking, sending) is the RFC's job, with opt-in grants and approvals.

Dogfood (real model turns, real T3)

Sandbox T3 from this branch (worktree .t3, vp run dev), provider = stock Hermes ACP (gpt-6.1-sol) behind the demo-connector proxy, project = an empty folder. Prompt, typed into the composer:

I'm heading to SFO later. Using my connected apps: search my Gmail for "invoices from Acme", check Uber prices from the Ferry Building to SFO Terminal 2, get driving directions from Home to the Ferry Building, and check GitHub pull request pingdotgg#128. Call each tool once, then show it all as one openui-v2 board (MailList, RideOptions, Route, PullRequest) with a two-sentence summary. Don't book anything.

Seven real turns across the session; every one called all four tools once and produced a board the inspector accepts ({kind: "ready"}), with no invented values. The recorded run: board on screen 35 s after send.

Found by dogfooding and fixed here, each with a test: grouped rows named the renamed servers ("Used editor gmail 63ea3519, …"); Hermes tool names carry the detail after a colon (editor_gmail_63ea3519.search_messages: invoices from Acme); the running and single-call labels bypassed the app sentence; list markers leaked into two cards.

Known gap, not fixed: Hermes runs the four calls in parallel, so the live row shows only the newest call (in the recording: Thinking → Tool → GitHub).

Verification

Check Result
vp test run client-runtime (appConnectors, openUiAppCards, work-log/presentation), apps/web/.../MessagesTimeline.logic, apps/mobile/src/lib/threadActivity 165 passed (commit 8: openUiAppCards 3 passed)
vp test run apps/web/src/components/chat 392 passed; 24 browser-mode files did not start here (no matching Playwright browser), CI runs them
vp test run --project browser OpenUiAppCards.browser.test.tsx (commit 8: every card renders; ride selection, route steps and PR commits work) 1 passed; the screenshot above is from this test
OpenUI lab smoke (stock @openuidev/* in an isolated install, renders web catalog incl. app cards) 9 passed
tsc --noEmit apps/web, apps/mobile, packages/client-runtime 0 errors
vp lint + vp fmt --check on touched files clean

Web build for the recording: built from the PR head on a GitHub runner (run), served locally against the branch server.

Not verified: iOS and the packaged desktop shell. Android: built in CI from this branch, run on an S25 (replayed and real turns). Commits were made with --no-verify because the repo-wide knip hook flags two *.node-test.ts files from the base branch (openUiCharts.node-test.ts, openUiMarkdown.node-test.ts); this PR's own tests are vitest.

Surfaces

  • Web and desktop: activity rows and cards. Mobile: same rows and cards in native views (typechecked, not device-run).
  • Providers: brand matching is provider-agnostic (MCP server name). Hermes rename handled; Codex/Claude/Cursor/OpenCode names pass through the same normalizer.
  • Contracts: none changed. Remote/relay/tunnel: nothing new crosses the wire; cards are message text.

Credits

OpenUI renders the boards. The PullRequest card and the film's GitHub sequence follow @pitus_'s "Generative UI #2 – Merge conflicts". Brand-colored app cards in place of a bare "thinking" row: @kvnloo.

Built with Claude Opus 5.5 in Hermes, driven from T3 Code.

Tool calls from known apps (Gmail, Google Calendar, Google Maps, Uber,
Lyft, GitHub, Slack, Spotify, DoorDash) now show the app's colored
monogram and a plain sentence ("Searching Gmail · invoices from Acme")
instead of the raw MCP server and tool name, on web and mobile. Grouped
rows name the apps ("Used Gmail, Uber, and Google Maps").

Matching is by MCP server name, so it works for every provider, and it
undoes Hermes's editor_<name>_<hex> rename. No logos are bundled.
…ests

Chart boards (openui-v2) can now hold four read-only app cards drawn in
their service's colors: RideOptions (Uber/Lyft, cheapest tagged),
MailList (Gmail), Route (Google Maps) and PullRequest (GitHub progress
track, conflict count and check chips). Taps stay local to the card;
nothing is booked, sent or merged. A merged pull request plays its fill
once on insertion and respects reduced motion.

The shared inspector rejects incoherent cards (a conflict state without
conflicts, a ready PR with failing checks, duplicate ride options) the
same way it rejects bad charts, on web and Android.
demo-connectors.mjs serves canned Gmail, Uber, Google Maps and GitHub
tools over MCP stdio (nothing is fetched, booked or merged).
acp-with-connectors.mjs wraps any ACP agent and adds them to each
session, so a sandbox provider instance gets the tools without editing
the agent's own config.
Dogfooding showed the running turn header and single-call rows still
read "Get pull request": they resolve their labels separately from the
row. All three label paths on web and mobile now use the shared app
activity label first, with the live status taking precedence.
Inside chat Markdown the cards' lists inherited bullets and numbers, so
the Route card showed "3. (3)" and mail rows had stray dots.
The row that shows the running tool now draws the app's monogram, so a
turn that is searching Gmail reads as Gmail while it happens, not only
after it finishes.
Cards now read like the apps they come from: ride options with a selected
row and a Request button that opens the ride app, mail rows with avatars and
attachments plus Open in Gmail, a step-by-step route with Open in Maps, and
a pull request card with a small commit graph and conflict marks after
@pitus_'s merge-conflicts card. New fields are optional, so existing boards
still parse; Android accepts them and shows the simpler layout.
…utes

App cards now copy each service's design system instead of sharing T3's
palette: Primer for GitHub, Base Web for Uber and Lyft, Material 3 for Gmail
and Google Maps, each in its own typeface and in light and dark.

- Route draws the path the maps tool returned, projected in Web Mercator onto
  the tool's basemap, so the line follows the road; no path, no map.
- PullRequest shows checks as a timeline when the tool reports timing, opens a
  failing check's detail, and ends with GitHub's merge-box line.
- RideOptions shows vehicle pictures and a sliding selection.
- Cards reflow at phone width (container queries) instead of clipping.
- Android renders the same cards natively (react-native-svg, embedded fonts).
- Demo connectors return a real OSRM route, timed checks and commits.
@github-actions github-actions Bot added the vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. label Nov 3, 2026
… in-app film

- A streaming openui-v2 board now renders its complete statements as they
  arrive, so cards appear one by one instead of showing source until the fence
  closes (web and desktop; Android still renders once complete).
- Replay (dev) gains an "App cards" scenario: four demo connector calls, then
  the board, so the real server, adapter and clients can be checked for free.
- test/appCardsFilm.renders.tsx shoots the launch film inside the real app
  (AppRoot on the fake environment), one PNG per frame on a virtual clock.
- Ride card hides its price note at phone width; Android dims the basemap in
  dark mode.
- test/harnessKits.renders.tsx mounts the demo board inside each kit's
  colours, fonts and radii (mapped onto T3's semantic variables) at 412 and
  760 px, light and dark, and fails if a phone-width thread scrolls sideways.
  Kits and their provenance live outside the repo.
- The demo board moves to test/fixtures/appCardsBoard.ts so the browser test
  and the preview share it.
- The app cards film starts its minute clock at the fixtures' time, so the
  sidebar no longer reads "230d".

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant