Public: anyone can read this; the crew can change it.
App page shows an adapter's last pull, next pull and cursor #10068
Check · Review · Land · Live done — Landed and live in production.
An app whose `forge.json` declares an `adapter` now gets an "Adapter" card on its studio page, in the side column. The tests for the card, the query plans, the pull schedule and app-page timing pass, and `tsc`, the gate and design-lint are clean. I didn't run the whole suite, and I didn't try the card on a running app.
shipped production is serving it deploy a168196
An app whose forge.json declares an adapter now gets an "Adapter" card on its studio page, in the side column. The tests for the card, the query plans, the pull schedule and app-page timing pass, and tsc, the gate and design-lint are clean. I didn't run the whole suite, and I didn't try the card on a running app.
What the card shows
- Source: the source name, or "Not named" when
forge.jsondoesn't give one, and how often the app is pulled. - Last pull: state, written/skipped/failed counts and when; a failed pull shows its reason instead. A pull whose cursor repeated the last one says "nothing new". Before the first pull it says "Not yet".
- Next pull: the last pull's time plus the schedule, or "Due now, once it is running" when that time has passed. Before the first pull it says "On the next sweep, once it is running".
- Cursor: the cursor of the last good pull.
- Every value goes through
html, so adapter-written text is escaped.
What moved
src/web/studio/adapter.tsis new.adapterCardViewis pure (facts in, HTML out) andadapterCarddoes the reads.src/web/studio/app.tsrenders the card in the side column.declaredAdapterinsrc/apps/pull_schedule.tsis now exported, so the page reads the adapter block the same way the scheduler does.
Tests
test/hot-reads-indexed.test.tshas a new test from the query plan showing that the last-pull read walkspull_run_appand the last-cursor read walkspull_run_app_ok, with no scan or temp b-tree.test/adapter-card.test.tsrenders the card (including hostile text), a failed pull, and the state before any pull.
Choices
- The card is shown only to people who can see the owner's facts, since pull reasons and cursors are operational detail. If you want it visible to every viewer of the page, that is one condition in
app.ts. - I left out a pull-history list and a "pull now" button, since the issue asks for neither.
The preview didn't fail: I changed nothing and pushed nothing. The log you pasted shows it checked out 3d6bf98, started, and ended with done: https://lab--10068--forge-trevato-apps.open-platform.sh. I ran /healthz on that preview just now and it answers 200.
The "no tests declared, none run" line is only the preview deploy saying forge.json has no app.test command. The tests run in the check block (fast, run, verify), not in the preview deploy.
Locally, tsc, scripts/gate.ts, design-lint and the tests for this change (adapter card, indexed reads, pull schedule, app-page timing) all passed before I pushed. The forge's check is the verdict on this push. If it fails, send me its log and I'll fix what it names.
The check failed only because the gate wants a "Try:" line in a commit message, and none of the commits on this pull had one. I added an empty commit whose message has that line, and pushed it. I did not re-run the gate, tsc or the tests here, because the code is unchanged. The forge's check on the new push will show whether the gate now passes.
The pull changes how the app page shows an adapter, and nothing else. The app page now shows an adapter's source, last pull, next pull and cursor (#10037); the code is in src/web/studio/adapter.ts and src/web/studio/app.ts.
Try: on the preview, open the app page of an app that has a running adapter. It should show the adapter's source, last pull, next pull and cursor.
I chose not to rewrite history to put the line on the original commit, since force pushes are refused.
🤖 Generated with Claude Code
Changes needed, in one round:
- Both doors: the card exists only on the web page. Add a terminal screen in src/tui/ (entry wired where AGENTS.md says) that shows the same facts, gated the same way (owner facts only), and have it read the same adapterCardView facts.
- Add docs/features/adapters-card.md (or extend an existing adapter feature doc) with the how-to.
- Design: the facts are wrapped in card(); principles 6 and 7 say a fact is a dl row, not a boxed card. Render the dl as plain rows under an h2, without card().
- Remove
void repo;and the unused repo parameter of adapterCard (dead code). - The adapter's declared-but-unreachable first-paint cost: declaredAdapter reads git on the page path; confirm it is cached by tip (it is) and add a timing note if not.
Box 1 left because of the card and the missing terminal door; boxes 2 and 3 are met.
All five of juniper's points are addressed and pushed to the same branch. bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts and the five test files I picked all pass. I did not run the full suite: scripts/fast.ts said the diff was too broad to name (1539 of 2326 files), and the forge runs the full check on push.
- Terminal screen.
- New src/tui/screens/adapter.ts, reached from the palette's per-app "Adapter" entry (adapterCommands, wired next to trafficCommands in workspace.ts).
- It shows the same five rows as the page, gated by canSeeOwnerFacts. A viewer who fails the gate, or an app with no app record, gets no entry. If the screen is reached anyway, it says the app declares no adapter.
- Both doors read one new module, src/apps/adapter_facts.ts: AdapterFacts, adapterFactsOf, everyWords, nextDue and the plain-text adapterRows.
- I put the shared read in src/apps/ rather than having the TUI import from src/web/, so the terminal doesn't depend on a web module. adapterCardView now only formats the facts as HTML.
- Docs.
docs/features/adapters-card.mdhas the how-to for both doors. - Design. The
card()wrapper is gone. The page now renders a<section>with an<h2>Adapter</h2>and plaindlrows. - Dead code.
void repo;and the unusedrepoparameter are removed, and the caller inapp.tsis updated. - First-paint cost.
declaredAdaptercaches the parsed spec by default-branch tip (declaredByTip). A repeat view costs one ref lookup plus the two limit-1 run reads, so I added no timing code. The doc says so.
Tests added in test/adapter-card.test.ts: the terminal rows match the card's facts, and the card is plain rows with no card class.
I did not add a test for the palette gate or for the screen's own rendering. The gate is a one-line canSeeOwnerFacts check, but if you want it pinned I can add one.
Try: open the app page of an app with an adapter on the preview. Or open its repository in the terminal and choose the palette's "Adapter" entry.
🤖 Generated with Claude Code
Security reads clean: facts are owner-gated on both doors, every value goes through html, reads are limit-1 on indexed queries. Fix before landing: (1) FIRST: the terminal door's gate has no test. Add a test that a guest or non-owner gets no 'Adapter' palette entry from adapterCommands and that the screen shows no rows for them, and that an owner does get the entry (the builder offered this; do it). A gate with no test that would fail without it is a miss. (2) The last-pull and next-pull wording is written twice (adapterRows and adapterCardView/lastPull/nextPull), so the doors can drift; build the phrases once in adapter_facts.ts as data and let the web only add markup. (3) Tidy: mid-file import in the test, [area][0]! in the screen, stray '...' in the doc.
An app whose forge.json declares an adapter now shows the adapter's source, how often it pulls, its last pull, its next pull and its cursor. They appear both on the app page and on a terminal screen. Both are for owner-fact viewers only (canSeeOwnerFacts).
Where things are
- The facts:
src/apps/adapter_facts.tsreads them once for both doors.adapterFactsOfloads the declared spec, the last run and the last good cursor.adapterFactRowsbuilds each row's wording once, as typed parts (text,muted,badge,time,code). - Web:
src/web/studio/adapter.tsonly turns those parts into markup. The app page shows them as plaindlrows under an<h2>Adapter</h2>in the side column, not in a card.adapterCardno longer takes the unusedrepoparameter, andapp.tsis updated to match. - Terminal:
src/tui/screens/adapter.tsprints the same parts as plain text viaadapterRows.adapterCommandsoffers a per-app "Adapter" palette entry while a repository with an app is open. It is wired next totrafficCommandsinworkspace.ts. - Docs:
docs/features/adapters-card.mdhas the how-to for both doors.
Tests
test/adapter-card.test.ts: values are escaped, the card is plain rows and not a card, and the terminal rows match the card's facts.test/adapter-tui.test.tstests the terminal gate. The owner gets the palette entry and the rows; a guest and a signed-in outsider get neither, and their screen never reads the facts. WithcanSeeOwnerFactsdropped, two of its three tests fail.adapterScreentakes the facts loader as a parameter, so that test can run without a git repository.
Checks run: bun scripts/gate.ts, bun x tsc --noEmit, bun scripts/design-lint.ts, and five test files (the two above plus studio-app-timing, studio-door and palette-entries) all pass. scripts/fast.ts named 1539 of 2326 test files, too many to run here, so the full suite is left to the forge's check.
Not done
- No test renders a real adapter's rows from git end to end.
declaredAdapteris already covered by the pull-schedule tests. - No timing code was added for the first paint. The parsed spec is cached by default-branch tip (
declaredByTip), so a repeat view costs one ref lookup plus two limit-1 indexed reads.
Try: on the preview, open /<owner>/<repo>/app for an app that has an adapter. Or open its repository in the terminal and choose "<owner>/<repo> — Adapter" from the palette.
🤖 Generated with Claude Code
Every earlier ask is met on this head. (1) The terminal gate now has a test: test/adapter-tui.test.ts shows the owner gets the palette entry and the rows, and a guest or signed-in outsider gets neither and the screen never reads the facts. (2) The wording is built once as typed parts in adapter_facts.ts (adapterFactRows). The web only adds markup and the terminal prints plain text. (3) The tidy items are gone: no mid-file import, no [area][0]!, no stray ellipsis in the doc. The card is a section with an h2 and dl rows, with no box, which fits principles 6 and 7. It renders server-side, so it works with JavaScript off. The how-to is in docs/features/adapters-card.md, not README. I did not run the tests or check the preview this round.
Two things to do next:
- Non-blocking: when nextDue is unreadable, nextPullParts returns [], which leaves an empty dd under Next pull. Return a muted "Unknown" instead.
- Non-blocking: add one end-to-end test that reads a real forge.json adapter block from git through adapterFactsOf, so the declaredByTip cache path is covered here and not only in the pull-schedule tests.
An app whose
forge.jsondeclares anadapternow gets an "Adapter" card on its studio page, in the side column. The tests for the card, the query plans, the pull schedule and app-page timing pass, andtsc, the gate and design-lint are clean. I didn't run the whole suite, and I didn't try the card on a running app.What the card shows
forge.jsondoesn't give one, and how often the app is pulled.html, so adapter-written text is escaped.What moved
src/web/studio/adapter.tsis new.adapterCardViewis pure (facts in, HTML out) andadapterCarddoes the reads.src/web/studio/app.tsrenders the card in the side column.declaredAdapterinsrc/apps/pull_schedule.tsis now exported, so the page reads the adapter block the same way the scheduler does.Tests
test/hot-reads-indexed.test.tshas a new test from the query plan showing that the last-pull read walkspull_run_appand the last-cursor read walkspull_run_app_ok, with no scan or temp b-tree.test/adapter-card.test.tsrenders the card (including hostile text), a failed pull, and the state before any pull.Choices
app.ts.Closes #10037