Apps live in places — the unified placement model, space-wide installs, and the pullback (#853)
Squashed history of PR #853. The through-line: replace the app framework's
tangle of placement flags with ONE vocabulary — `places` — carry it through
routing, the UI, and the member-app pipeline, then deliberately pull the
member-facing surface back behind a flag while the machinery stays live.
The places model
- Every app declares `places`: any non-empty subset of ["you","chat","space"].
Legacy flags (personal/groupCapable/spaceCapable) and the older `scope` enum
are accepted as INPUT at one funnel (normalizePlaces) then deleted, so nothing
downstream reads a capability flag and a stale one can't disagree with places.
- Space-wide apps: one shared instance in a hidden all-members space thread,
its commands usable from every chat; a sidebar "space band" home; install via
the Apps sheet or `/cmd @everyone`. The Vault is space-capable (a team
credential shelf); /gif asks for one Giphy key per space, asked once.
- The place-flag family — @me / @here / @everyone — with the "/" typeahead
teaching the flags as selectable rows, in flowing per-row sentences.
- The Apps sheet is focus-and-explain: one directory, each app naming its
places, the verb on a footer action.
The pullback (member-facing UI hidden, machinery live)
- Shipped apps park at ["you","chat"]; the multi-place UI hides behind the
PLACES_UI device flag (default off) so two same-named rows can't appear in
the thread list. Space band, install sheets, and space routing stay live and
tested via synthetic fixtures.
The hook split
- chatMessage now means only "companion Chat view"; groupFeedMessage is the
feed-only projection into group chats (fallback: chatMessage(_, {group:true})).
Replaces the mis-named groupChatOnly flag — the hook shape is the declaration.
Member apps
- May declare any non-empty places subset (space-only + listed:false, the /gif
shape, included). A documented compat policy (docs/member-apps.md § Legacy
formats) + App Studio "older format" flag with one-tap in-place Upgrade.
Security + review fixes
- Member commands that collide with an existing verb (built-in or another app)
are rejected at the load-time gate — a crafted blob could otherwise shadow
/todo or /help on every client and route typed args into a hijacker-readable
log. Plus confirmed lows: @here stripped on chat-only apps, spaceAppInfo
honours the admins-only policy, installs registry declares places:[].
Docs: apps/README, space-apps.md, building-apps.md, building-member-apps.md,
member-apps.md, app-credentials.md, CLAUDE.md all speak places; the bot's
served guide teaches it. Tests + event vectors move with the contract.
Co-authored-by: Claude <noreply@anthropic.com>
.claude/skills/verify/SKILL.md+31 −3CLAUDE.md+25 −6bot_appbuild_test.go+11 −9changelog/2026-07-14-poll-whiteboard-widget-bar.md+7 −0changelog/2026-07-15-app-studio-upgrade.md+6 −0changelog/2026-07-15-command-place-flags.md+7 −0changelog/2026-07-15-gif-space-key.md+9 −0changelog/2026-07-15-typeahead-install-offers.md+9 −0checkpoint_test.go+1 −1db.go+153 −4docs/app-credentials.md+275 −0docs/building-apps.md+42 −39docs/building-member-apps.md+73 −11docs/member-apps.md+40 −2docs/space-apps.md+258 −0internal/bot/appbuild.go+12 −6ios/LChatCore/Sources/LChatCore/GroupApps.swift+2 −2ios/LChatCore/Tests/LChatCoreTests/GroupAppsVectorTests.swift+1 −1main.go+45 −15miniapps.go+3 −0push.go+7 −0spacethread.go+135 −0spacethread_test.go+357 −0test/apps.test.mjs+701 −58test/check-app.test.mjs+25 −5test/event-vectors.test.mjs+17 −0test/event-vectors/groupapps.json+11 −3test/event-vectors/index.js+2 −0test/event-vectors/spaceapps.json+166 −0test/sandbox-bridge.test.mjs+1 −1test/sandbox.test.mjs+88 −11tools/check-app.mjs+20 −7web/pages/index.html+7 −1web/static/apps.js+635 −134web/static/apps/README.md+169 −82web/static/apps/admin.js+11 −1web/static/apps/appstudio.js+31 −4web/static/apps/board.js+4 −1web/static/apps/bookmarks.js+17 −11web/static/apps/chat.js+4 −12web/static/apps/connect.js+2 −1web/static/apps/dnsimple.js+1 −1web/static/apps/emoji.js+1 −2web/static/apps/gif.js+65 −23web/static/apps/github.js+1 −1web/static/apps/hubspot.js+1 −1web/static/apps/linear.js+1 −1web/static/apps/memory.js+1 −1web/static/apps/poll.js+21 −1web/static/apps/remind.js+11 −4web/static/apps/setup.js+1 −1web/static/apps/stripe.js+1 −1web/static/apps/todo.js+13 −4web/static/apps/vault.js+26 −21web/static/apps/views.js+1 −1web/static/apps/welcome.js+1 −1web/static/groupactions.js+378 −29web/static/newchat.js+64 −11web/static/sandbox-frame.js+18 −8web/static/sandbox.js+70 −9web/static/shell.js+536 −77web/static/shellstate.js+16 −0web/static/style.css+79 −0
.claude/skills/verify/SKILL.mdLoading diff…
CLAUDE.mdLoading diff…
bot_appbuild_test.goLoading diff…
changelog/2026-07-14-poll-whiteboard-widget-bar.mdLoading diff…
changelog/2026-07-15-app-studio-upgrade.mdLoading diff…
changelog/2026-07-15-command-place-flags.mdLoading diff…
changelog/2026-07-15-gif-space-key.mdLoading diff…
changelog/2026-07-15-typeahead-install-offers.mdLoading diff…
checkpoint_test.goLoading diff…
db.goLoading diff…
docs/app-credentials.mdLoading diff…
docs/building-apps.mdLoading diff…
docs/building-member-apps.mdLoading diff…
docs/member-apps.mdLoading diff…
docs/space-apps.mdLoading diff…
internal/bot/appbuild.goLoading diff…
ios/LChatCore/Sources/LChatCore/GroupApps.swiftLoading diff…
ios/LChatCore/Tests/LChatCoreTests/GroupAppsVectorTests.swiftLoading diff…
main.goLoading diff…
miniapps.goLoading diff…
push.goLoading diff…
spacethread.goLoading diff…
spacethread_test.goLoading diff…
test/apps.test.mjsLoading diff…
test/check-app.test.mjsLoading diff…
test/event-vectors.test.mjsLoading diff…
test/event-vectors/groupapps.jsonLoading diff…
test/event-vectors/index.jsLoading diff…
test/event-vectors/spaceapps.jsonLoading diff…
test/sandbox-bridge.test.mjsLoading diff…
test/sandbox.test.mjsLoading diff…
tools/check-app.mjsLoading diff…
web/pages/index.htmlLoading diff…
web/static/apps.jsLoading diff…
web/static/apps/README.mdLoading diff…
web/static/apps/admin.jsLoading diff…
web/static/apps/appstudio.jsLoading diff…
web/static/apps/board.jsLoading diff…
web/static/apps/bookmarks.jsLoading diff…
web/static/apps/chat.jsLoading diff…
web/static/apps/connect.jsLoading diff…
web/static/apps/dnsimple.jsLoading diff…
web/static/apps/emoji.jsLoading diff…
web/static/apps/gif.jsLoading diff…
web/static/apps/github.jsLoading diff…
web/static/apps/hubspot.jsLoading diff…
web/static/apps/linear.jsLoading diff…
web/static/apps/memory.jsLoading diff…
web/static/apps/poll.jsLoading diff…
web/static/apps/remind.jsLoading diff…
web/static/apps/setup.jsLoading diff…
web/static/apps/stripe.jsLoading diff…
web/static/apps/todo.jsLoading diff…
web/static/apps/vault.jsLoading diff…
web/static/apps/views.jsLoading diff…
web/static/apps/welcome.jsLoading diff…
web/static/groupactions.jsLoading diff…
web/static/newchat.jsLoading diff…
web/static/sandbox-frame.jsLoading diff…
web/static/sandbox.jsLoading diff…
web/static/shell.jsLoading diff…
web/static/shellstate.jsLoading diff…
web/static/style.cssLoading diff…