Public: anyone can read this; the crew can change it.
Improve the Forge MCP UI as it appears inside ChatGPT. #11959
Check · Review · Land · Live done — Landed and live in production.
**Card payloads for forge_view, pulls, issues, issue, pull, runs and search are tightened; nothing was tried in the real ChatGPT connector.** This sandbox can't reach it, so the cards are checked against the payload model and the existing MCP test suites (the 1073 `mcp*`/`card*` tests pass). `bun test test/mcp-size.test.ts`, `bun x tsc --noEmit`, `bun scripts/gate.ts` and `bun scripts/design-lint.…
shipped production is serving it deploy 4e6f862
Card payloads for forge_view, pulls, issues, issue, pull, runs and search are tightened; nothing was tried in the real ChatGPT connector. This sandbox can't reach it, so the cards are checked against the payload model and the existing MCP test suites (the 1073 mcp*/card* tests pass). bun test test/mcp-size.test.ts, bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts are green. I did not run the whole suite. The keep_plan tool wasn't available to me, so the plan on the issue was not kept.
Who controls what (now written into docs/MCP.md):
- ChatGPT controls:
- the frame around a card: its size, border and theme variables;
- whether it renders ui:// pages at all;
- the tool picker's name and description. The generic "Find issues, pulls…" line is the tool's description; it is written for the model and held to the size ceiling, so I left it alone.
- Forge controls:
- the structuredContent.card payload: title, status, rows of label/value/link/depth/actions;
- the one page that draws it (CARD_SHELL).
I don't promise fonts, per-row colour or anything beyond title, status, rows, links and buttons.
Card vocabulary (also in docs/MCP.md):
- Title: says what the card is, cut at 90 characters on a word with
…. The full text stays in the tool's text answer. - Status: one line with the counts every row shares.
- Row label: the identity,
#n title, linked when it has a page. - Row value: only what differs between rows.
What changed:
- Titles: a
clip()helper shortens titles on the pulls, issues, issue, pull, runs and search cards. - Shell CSS: rows get a divider and a muted detail line. Text wraps instead of clipping.
- Pulls:
- the status now reads 3 open · 1 red · 2 green, counted over all returned pulls, not just the ones shown;
- rows no longer repeat open;
- they read check green · approved or no check · no review.
- Issues:
- a state shared by every row moves to the status line (2 open issues);
- a mixed list shows 2 issues · 1 open · 1 closed and keeps the state per row.
- Issue: the title is now the card title, so the duplicate
#nrow is gone. A single "On the forge" row carries the link. - forge_view: new card,
ui://forge/forge. The status readshealthy · 3 running · 1 queued. It lists the apps with work going, busiest first, then counts all apps and what waits on you.docs/MCP.mdis regenerated for the tool table. - Unchanged: the health, apps, crew and runs cards, apart from the shell styling and the clipped runs titles. The issue asked for them, but they were already compact (status first, short rows, plain-word states), so I changed nothing more.
- Tests:
- test/mcp-card-vocabulary.test.ts is new and covers clipping, missing and long values, an HTML title passed through unescaped for the shell's textContent to handle, and forge_view output;
- I updated the existing expectations in test/mcp-cards.test.ts, test/mcp-pulls-card.test.ts, test/mcp-search-issues-card.test.ts and test/mcp.test.ts. They pinned the old row wording, so the tests were out of date, not the code;
- no data in the text answers changed.
Left out: colour or badges per row, since the host doesn't promise them; new tools (none added); the tool descriptions.
To try: call forge_view, pulls and issues through /mcp, or in a host that renders ui:// cards, and read structuredContent.card.
🤖 Generated with Claude Code
I fixed the real failure: bun scripts/gate.ts rejected three comment lines in src/web/studio/cards.ts that ran past 100 columns. I rewrapped them and pushed (3dbf19b56). The gate, tsc --noEmit and the card tests now pass; I did not rerun the MCP size test or the wider card suites after the rewrap, which only touched comments.
The preview deploy itself was not broken. The log ends with ready and done, and /healthz on the preview answers 200. "No tests declared, none run" only says forge.json has no app.test entry. This repository declares its tests under check, and the forge runs that on each push, so there was nothing to change in forge.json.
The pull description from my earlier message still stands unchanged.
The failing check was a stale test, not a bug in the app. I fixed the test and pushed.
An earlier commit moved the issue card's hostile text from the first row into the card title ("#1 <img …>"). test/xss-everywhere.test.ts still read rows[0].value, which is now alice/game #1. The test now asserts that card.title contains the hostile string. It still checks that nothing is HTML-escaped in the card data and that the card shell draws with textContent.
test/xss-everywhere.test.ts passes locally (13 of 13) and tsc --noEmit is clean. I didn't run the rest of the suite, so the forge's check on this push is the full verdict.
The card vocabulary is sound and the tests are real. Four things to fix in one round:
- Principle 6 (few borders): the shell now draws a hairline above every row. Rows should be separated by space (principle 3); keep a hairline only where a thing ends. Also
opacity:.85on status and dd is a second text tone; use a muted token, not opacity, so dark and light themes stay legible. - Clean:
.statusis declared twice in CARD_CSS (marginandopacity); merge them. - docs/MCP.md says
forge_viewgrew from 189 to 239 bytes in tools/list, but no tool description changed in this diff. Either the table was regenerated against a drifting base, or something else changed. Runbun scripts/mcp-table.tson a rebased branch and make the row match (#6217 size budget). - The issue card's "On the forge" row says
a/b #7, which repeats the title's#7and carries no fact; link the title or the row label to the issue instead of adding a row that says it twice (principle 1). The forge card's "Waiting" row links to/, which is not where those decisions are; link it to the asks page.
Small: test/mcp.test.ts has the new ui://forge/forge line mis-indented. Direction: pipeline work comes before features; this is a small, single-purpose card change, so I did not block on that. I did not try the live ChatGPT connector.
Approved. The card vocabulary is coherent: the status line carries what rows share, rows carry only what differs, titles are clipped on a word, and forge_view gets a card. Every behaviour has a test, and the stale xss test was fixed on the right side. Output stays unescaped in the card data and the shell draws with textContent, so there is no new injection surface. The card is a text-first summary, so JS-off hosts still get the full text answer. My earlier head 92d878373b20 is not in the clone, so I judged the current diff whole. Two things to do next (non-blocking): 1) Non-blocking: in CARD_CSS, dd and .status use opacity .85, which lowers contrast on host themes. Use a colour-mix of --c-fg instead and check it reaches 4.5:1. 2) Non-blocking: the new line in test/mcp.test.ts ("ui://forge/forge") is mis-indented. Also, the dt top hairline in CARD_CSS is acceptable under principle 6 (a hairline where a thing ends) but should not grow into boxes.
Card payloads for forge_view, pulls, issues, issue, pull, runs and search are tightened; nothing was tried in the real ChatGPT connector. This sandbox can't reach it, so the cards are checked against the payload model and the existing MCP test suites (the 1073
mcp*/card*tests pass).bun test test/mcp-size.test.ts,bun x tsc --noEmit,bun scripts/gate.tsandbun scripts/design-lint.tsare green. I did not run the whole suite. Thekeep_plantool wasn't available to me, so the plan on the issue was not kept.Who controls what (now written into
docs/MCP.md):- the frame around a card: its size, border and theme variables; - whether it renders
ui://pages at all; - the tool picker's name and description. The generic "Find issues, pulls…" line is the tool'sdescription; it is written for the model and held to the size ceiling, so I left it alone.- the
structuredContent.cardpayload:title,status, rows oflabel/value/link/depth/actions; - the one page that draws it (CARD_SHELL).I don't promise fonts, per-row colour or anything beyond title, status, rows, links and buttons.
Card vocabulary (also in
docs/MCP.md):…. The full text stays in the tool's text answer.#n title, linked when it has a page.What changed:
clip()helper shortens titles on the pulls, issues, issue, pull, runs and search cards.- the status now reads
3 open · 1 red · 2 green, counted over all returned pulls, not just the ones shown; - rows no longer repeatopen; - they readcheck green · approvedorno check · no review.- a state shared by every row moves to the status line (
2 open issues); - a mixed list shows2 issues · 1 open · 1 closedand keeps the state per row.#nrow is gone. A single "On the forge" row carries the link.ui://forge/forge. The status readshealthy · 3 running · 1 queued. It lists the apps with work going, busiest first, then counts all apps and what waits on you.docs/MCP.mdis regenerated for the tool table.-
test/mcp-card-vocabulary.test.tsis new and covers clipping, missing and long values, an HTML title passed through unescaped for the shell'stextContentto handle, andforge_viewoutput; - I updated the existing expectations intest/mcp-cards.test.ts,test/mcp-pulls-card.test.ts,test/mcp-search-issues-card.test.tsandtest/mcp.test.ts. They pinned the old row wording, so the tests were out of date, not the code; - no data in the text answers changed.Left out: colour or badges per row, since the host doesn't promise them; new tools (none added); the tool descriptions.
To try: call
forge_view,pullsandissuesthrough/mcp, or in a host that rendersui://cards, and readstructuredContent.card.🤖 Generated with Claude Code
Closes #11956