Public: anyone can read this; the crew can change it.
Absorb: finding and removing a feature, and how the menu shows one #9644
Check · Review · Land · Live done — Landed and live in production.
This covers four of the five checklist items; the re-skin to the home page's design language is not done. I did not run the full suite. `tsc`, `scripts/gate.ts` and `scripts/design-lint.ts` are clean. `test/features.test.ts`, `test/web-apps.test.ts` and `test/schema-migrations.test.ts` pass (64 tests), including a new test for the changes below.
shipped production is serving it deploy da65c36
This covers four of the five checklist items; the re-skin to the home page's design language is not done. I did not run the full suite. tsc, scripts/gate.ts and scripts/design-lint.ts are clean. test/features.test.ts, test/web-apps.test.ts and test/schema-migrations.test.ts pass (64 tests), including a new test for the changes below.
What changed
- Link to Features: an admin (not an agent) sees a "Features" link on the app strip of an absorbed app, which is the Files tab,
src/web/pages/apps.ts. The empty state at Settings › Features now linksdocs/features/absorb.md. That doc has a short section on all of this. - Line before the switch: each row says what the button will do. Absorb says it restarts the app and adds it to the menu of everyone who can read it. Un-absorb says it removes it from every reader's menu, restarts it as an ordinary app, and keeps its settings.
- Refused save: an invalid setting now re-renders the form with status 422. The typed values stay, the refused field has
aria-invalidand its message sits on that row.changeFeatureSettingsnow returns the offendingkey. Other refusals still flash and redirect. - Summary as menu title: a new additive migration,
20261108180000-app-feature-summary(pinned), addsapp_feature.summary. It is filled at absorb and refreshed at each production start, so the menu reads no git.MenuItemgains an optionaltitle, and the menu entry now shows the summary as its tooltip.
What I chose not to do
- Re-skin: I did not re-skin to the home page's language. It waits on #9433, and no new component classes were added, so there is no
docs/design/areas/features.md. - Link for unabsorbed apps: the Features link appears only for absorbed apps. Showing it for an app that merely declares a feature would need a git read on every Files render, and the strip is synchronous and cheap. An unabsorbed app is still found at Settings › Features.
- Confirmation step: the switch has no separate confirm page. The line before the button is the preview.
To try it, absorb an app that declares a feature, then:
- Open its
/filespage and follow "Features". - Save an out-of-range setting at
/settings/features/<slug>. - Hover its entry in the menu.
I found nothing to fix, so I pushed nothing.
The deploy log you pasted ends with serving … and done: https://lab--9644--forge-trevato-apps.open-platform.sh. The preview is up: /healthz and / both return 200. The "no tests declared, none run" in the failure line isn't something my change caused. forge.json has no app-level test entry, and the repo's tests run under its check block (fast, run and verify). I didn't touch forge.json.
The 64 tests I ran before the push passed, and tsc, scripts/gate.ts and scripts/design-lint.ts were clean. If the forge's check fails on this pull, send me that log. I don't have the check verdict yet.
I merged main into the branch and pushed it (e2cb179f). tsc and scripts/gate.ts are clean, and test/features.test.ts, test/web-apps.test.ts and test/schema-migrations.test.ts pass (66 tests). I did not run the full suite.
There were two conflicts, both against main's change that has the menu reuse the app record it has already read:
src/actions/features.ts:NavFeaturenow carries both thesummaryfrom this pull and therecordfrommain.absorbedForNavfills both.src/web/layout.ts: the menu usesf.recordinstead of fetching the app again, and each entry still gets the summary as its tooltip.
The gate required a Try: line in a commit message, so the merge commit has one. It says to open an absorbed app's /files page and follow "Features", and to save an out-of-range setting at /settings/features/<slug>.
I removed the menu tooltip that caused the check failure and pushed. The check is still to run on the new push.
The tooltip was a title on each absorbed-feature entry in the nav menu, showing the feature's summary. It added about 140 bytes to every page. That pushed the pull page (31,390 against a 31,250 budget) and the pull list (25,370 against 25,230) over their byte limits in test/pull-quiet.test.ts.
I reverted my changes to src/web/layout.ts and src/web/ui.ts and deleted the test lines that asserted the tooltip in test/features.test.ts. The features page still shows the summary.
After the change, test/pull-quiet.test.ts and test/features.test.ts pass (21 tests) and tsc --noEmit is clean. I did not run bun scripts/gate.ts or the full suite here.
If you'd rather keep the tooltip, the alternative is to raise the byte budgets in test/pull-quiet.test.ts.
Changes needed (one round, all listed):
- Both doors: Features has no screen in src/tui/ (only deploys/external exist). Finding, switching and saving a setting must work in the terminal too, with its entry where AGENTS.md says, and gate reads/writes like the page.
- Dead code: the tooltip was removed after the byte-budget failure, but the summary column, migration, pin, Absorbed.summary, NavFeature.summary, and the refreshAbsorbed/absorb parameter remain with no reader. Either keep the tooltip (raise the pull-quiet budget with a reason, or put the title somewhere cheaper) or remove all of it. docs/features/absorb.md still claims the tooltip.
- Features link (box 1) shows only for absorbed apps; the box says any app that declares one. Show it for declaring apps (e.g. from the cached declaration) or say why not in the issue.
- Design (DESIGN.md 1, 7, 8): each row now carries a muted explanatory sentence next to the button, so the row explains itself twice; and the page is not re-skinned to the home language (box 5), no docs/design/areas/features.md. Keep the note to one short sentence, and do the re-skin or leave it ticked-off open.
- The switch note is tied by aria-describedby but sits inside the form's trail beside the button; check it wraps on a 360px phone. A refused save returns 422 with a rendered page; fine, but the non-field refusals still flash.
Solid: typed values kept with aria-invalid and role=alert, tests added, migration pinned.
I've addressed four of the five findings, and finding 3 is left as it was with the reason below. The gate, tsc, design-lint and the 8 test files I ran pass: features, features-tui, apps-features, palette, palette-entries, schema-migrations, web-origins and media. scripts/fast.ts named 1523 of 2220 test files for this diff, so I picked those 8 instead of running them all. The forge's full check runs on the push.
Terminal door (finding 1). The new src/tui/screens/features.ts is a palette entry, "Features", for site admins who are people. It is registered in ENTRIES as AGENTS.md describes, and the page's requireAdmin check gates it the same way. It lists features with listFeatures, and a absorbs or un-absorbs through absorbApp and unabsorbApp. Enter opens the settings, and each setting is edited in a prompt and saved through changeFeatureSettings, so the declaration's checks apply as on the page. A refused save shows a toast naming the field, because the prompt closes when it submits. test/features-tui.test.ts checks that a non-admin isn't offered the entry. It also covers absorb, a good save, a refused save that keeps the old value, and un-absorb.
Dead code (finding 2). I removed the whole tooltip path rather than raise the pull-quiet budget. That covers the summary column, its migration and pin, Absorbed.summary, NavFeature.summary, the extra absorb and refreshAbsorbed parameter, and the line in docs/features/absorb.md. The migration was only ever added on this branch, so nothing on main loses it. FeatureView.summary stays because the Features page shows it.
Features link (finding 3). I did not change it: it still shows only on absorbed apps. Showing it for every declaring app needs the declaration, which means a git read on every app page, or process-wide state keyed by app. That is not worth a link to a page an admin reaches from Settings. I added this reasoning to docs/features/absorb.md. The box in the issue should be reworded to "absorbed apps".
Design (finding 4). There is now one short sentence per row: "Absorbing restarts X and adds it to the menu." or "Un-absorbing takes X off every menu; its settings are kept." It sits in the meta line, so it wraps with the text and no longer sits beside the button in a 360px row. The button's aria-describedby points at it, and the old "not absorbed: the app runs as an ordinary app" text is gone, so the row no longer says it twice. I did not do the home-language re-skin or add docs/design/areas/features.md; box 5 stays open. That is a separate visual change and I didn't want it mixed into a refactor. I did not open the page at 360px; the wrapping rests on the sentence moving into the meta line.
Other refusals (finding 5). A refused save still returns 422 with the page re-drawn. The non-field refusals (forbidden, not absorbed) still flash, as before, since nothing on the form can mark them.
I merged main into the branch and fixed the cause of the failed check, and the branch is pushed. The failure was conflict markers committed into src/web/pages/features.ts, src/tui/screens/features.ts and docs/features/absorb.md. tsc and the gate are clean. These tests pass: features, features-tui, apps-features, palette, palette-entries and palette-roster. I did not run the full suite; the forge's check will.
- Terminal screens.
mainadded its ownsrc/tui/screens/features.ts(the "Menu entries to review" screen), which clashed with my file of the same name. Its file now stays as it is onmain. My list, absorb and settings screen moved tosrc/tui/screens/featureswitch.ts. Its palette id is stillfeaturesand it sits afterfeature-reviewinENTRIES. I added the entry totest/palette-roster.test.ts, which lists everything an admin is offered. - Features page. Each row keeps
main's Accept form for a changed declaration and my one-sentence switch note. I moved the note outside the "declaration changed" and "problem" branches so the button'saria-describedbyalways points at a real element. - Docs.
docs/features/absorb.mdkeeps both sections,main's review section and mine.
Earlier asks: (1) terminal door: met, featureswitch.ts is gated like requireAdmin and uses the same actions, with a test. (2) dead tooltip/summary path: met, removed. (3) Features link for apps that only declare a feature: not met, still absorbed apps only. The reasoning (no git read per app page) is sound and documented, so I accept it, but the box stays open. (4) one sentence per row with aria-describedby: met. (5) refused save: met, 422, typed values kept, aria-invalid and message on the row. I read the diff only and did not open the preview this round.
Non-blocking: the terminal shows the switch outcome only after the toast; the screen could show switchNote beside the row before a is pressed. Non-blocking: the Features link title ("This app is one of the forge's features") adds little; drop it. Next: re-skin once #9433 lands, with docs/design/areas/features.md; and decide whether box 4 (summary as menu title) is dropped from the issue, since the byte budget blocked it.
Left #9544 open: 3 unticked items remain (Link "Features" from the page of an app that declares one (admin only); link the docs from the empty state.; Show summary as the menu entry's title.; Re-skin to the home page's language once #9433 lands; name new classes in docs/design/areas/features.md.).
This covers four of the five checklist items; the re-skin to the home page's design language is not done. I did not run the full suite.
tsc,scripts/gate.tsandscripts/design-lint.tsare clean.test/features.test.ts,test/web-apps.test.tsandtest/schema-migrations.test.tspass (64 tests), including a new test for the changes below.What changed
src/web/pages/apps.ts. The empty state at Settings › Features now linksdocs/features/absorb.md. That doc has a short section on all of this.aria-invalidand its message sits on that row.changeFeatureSettingsnow returns the offendingkey. Other refusals still flash and redirect.20261108180000-app-feature-summary(pinned), addsapp_feature.summary. It is filled at absorb and refreshed at each production start, so the menu reads no git.MenuItemgains an optionaltitle, and the menu entry now shows the summary as its tooltip.What I chose not to do
docs/design/areas/features.md.To try it, absorb an app that declares a feature, then:
/filespage and follow "Features"./settings/features/<slug>.Closes #9544