This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
Commit f962cdc
feat(app-shell): the suggested-bindings panel shows each suggestion's state and revokes an acceptance (part of objectui#7611) (#12148)
Part of #7611
Clause-②: yes (widening)
**Draft by dispatch** (objectui AGENTS.md §9 ②). This is C9 part 2: the
`SuggestedBindingsPanel` follow-up recorded in 6110524912 and claimed by
the amendment 6110601135, after objectstack#22854 (stage U2,
`d112087b8980`). The epic PM of objectstack#15194 moves this PR. The
rest of #7611 stays open; part 1 landed as `0df67f237c5a`.
## What objectui now shows
On a framework at objectstack `main` `d112087b8980` or later, a
`manage_metadata` holder's accept takes effect for the whole deployment,
and a dismiss on an accepted row revokes it (objectstack#22801 → A). The
panel on the marketplace package page and in the Studio Access pillar
used to list `pending` rows only and dropped `bindingCreated`. It now
lists every suggestion of the package (`GET
/api/v1/security/suggested-bindings?packageId=…`, no status filter) and
draws each in the state the server records:
| Row on the wire | The panel shows | Actions |
|---|---|---|
| `status: pending`, anchor `everyone` | the existing prompt | Accept,
Dismiss |
| `status: pending`, anchor `guest` | the prompt, and "Cannot be granted
here: the Guest position takes no sets from accepted suggestions." |
Dismiss only |
| `status: confirmed`, `deployment_decision: true` | "SET was accepted
for all signed-in users (the Everyone position)." | Revoke |
| the same row, after this session's accept answered `200` with
`bindingCreated: false` | "SET was accepted but is not in force yet: the
Everyone position did not change on this request. The server log says
why." | Revoke |
| `status: dismissed` | "SET is not granted: the suggestion was
dismissed or revoked." | none |
| `status: confirmed`, `deployment_decision: false` (the baseline names
the set) | not drawn | none |
- **Revoke** is the existing dismiss call (`POST …/:id/dismiss`) on an
accepted row. The success toast says the acceptance was revoked.
- **Accept** shows the existing success toast only when `bindingCreated`
is `true`. On `false` it shows the not-in-force line as a warning toast,
and the row keeps Revoke, because the acceptance is recorded.
`confirmSuggestedBinding` now returns `{ suggestion, bindingCreated }`
instead of dropping the flag.
- **Refusals.** A `403` or `409` on Accept, Dismiss or Revoke shows the
server's own `error.message`, never a silent no-op. The panel then reads
the list again, so the row shows what the server holds.
- **Kept:** a `403` on the list (a caller without `manage_metadata`)
still renders nothing. The list's `.catch` in the panel's mount effect
still swallows the refusal.
- The accepted line claims the decision, not that the set is in force.
The list does not say whether `everyone` carries an accepted set, so
"not yet in force" lasts until the panel re-reads. See the acceptance
notes.
- **Styling.** The panel keeps its amber look while a row awaits a
decision. With only decided rows it uses the muted look. Each state has
its own Lucide shield icon.
### Why a `guest` row shows the reason instead of rendering nothing
The server refuses a `guest` accept with `409`, and still accepts a
dismiss of a pending `guest` row. The panel therefore draws no Accept,
says why, and keeps Dismiss. This follows the convention part 1 of this
card set (`0df67f237c5a`): a caller the server would refuse gets no
affordance, and the page says why. It also follows objectui#12109's rule
that an affordance the server refuses is not drawn. Rendering nothing
would leave a pending row that no surface can show or dismiss. No
producer emits a `guest` suggestion today, so this branch is dormant
until one does.
### Why a baseline row is not drawn
Its dismiss answers `409` ("this surface does not decide the baseline"),
and its confirm answers `409` too. No action on it can succeed, and
nothing is pending. Before this change the panel did not draw it either.
## Measured against objectstack `main`
The measurement booted a showcase from a detached objectstack worktree
at `55382dc02a` (its `origin/main`, which contains `d112087b8980`). It
ran through a throwaway, untracked dogfood-style file, deleted after the
run, so `git status` in that worktree is clean. The driver was
`sqlite-wasm`. Readings, in order:
- With a vanilla `SecurityPlugin` (so `showcase_member_default` is a
package suggestion), the list row is `pending`, with
`deployment_decision: false`, `resolved_by: null` and `anchor:
"everyone"`.
- The accept answers `200`, `{ data: { suggestion, bindingCreated: true
} }`. The suggestion is `confirmed` with `deployment_decision: true` and
`resolved_by` set.
- **The list after the accept returns the same row: `status:
"confirmed"`, `deployment_decision: true` (a JSON boolean) and
`resolved_by` set.** It has no field that says whether the set is in
force.
- A second accept answers `409 SUGGESTION_STATE`: "… is already
confirmed — only pending suggestions can be accepted."
- The dismiss on the accepted row answers `200`, and the suggestion is
`dismissed` with `deployment_decision: true`. The list then shows the
same. **A revoked row and a dismissed pending row are identical on the
wire.**
- A second dismiss answers `409`: "… is already dismissed — only a
pending or accepted suggestion can be dismissed." An accept on that row
answers `409` ("… only pending suggestions can be accepted").
- A signed-up member's list answers `403 PERMISSION_DENIED`: "[Security]
Access denied: listing audience-binding suggestions requires the
`manage_metadata` capability. …"
- On the stock showcase, the same set is a baseline row: `confirmed`,
`deployment_decision: false`, `resolved_by: null`. Its dismiss answers
`409`: "… is observed, not accepted: the deployment's baseline gives the
set to everyone, and this surface does not decide the baseline."
`bindingCreated: false` on a `200` could not be produced through the
public door on that boot: the confirm and the derivation ask the same
gates. It is NOT MEASURED live here. objectstack pins it in its unit
suite, and #22854's ablation showed the flag turning `false`. The
panel's handling is pinned below against the documented envelope.
## Tests
The new
`packages/app-shell/src/components/SuggestedBindingsPanel.test.tsx` has
15 cases. It renders the real panel and the real `suggestedBindingsApi`
client over a stubbed `fetch` that answers the server's envelopes, with
row shapes and messages taken from the measurement above. There was no
test of this component before. The file follows app-shell's convention
for a component with a stubbed transport: `vi.stubGlobal('fetch', …)`,
`sonner` mocked, and `@object-ui/components` imported at module scope.
- One test per state: pending, accepted, dismissed or revoked, baseline
not drawn (with a control that a baseline-only panel renders nothing),
and `guest` (no Accept, the reason shown, Dismiss kept). One test pins
that a `403` on the list renders nothing, and one that the list asks for
every status.
- One test per action: Accept with `bindingCreated: true`; Accept with
`bindingCreated: false` (not in force, a warning and no success toast);
Dismiss; Revoke (the dismiss route, never confirm); Revoke refused `409`
(the message is shown, the list is re-read, and the row takes the
server's state); Accept refused `403`; and Revoke refused `403` followed
by a list `403` (the message is shown, then the panel renders nothing).
**Ablation** (one-time, not kept). Each mutation went through
objectstack's `scripts/ablation-replace.mjs`, which checks that the
anchor hit and that the restore holds. Each restore read blob == HEAD
with `git diff HEAD` empty.
- `bindingCreated: data?.bindingCreated === true` → `bindingCreated:
true`: exactly the not-in-force test went red (1 failed, 14 passed).
- `return s.anchor === 'guest' ? 'guest' : 'pending';` → `return
'pending';`: exactly the `guest` test went red (1 failed, 14 passed).
- `s.status === 'confirmed' && s.deployment_decision === true` →
`s.status === 'confirmed'`: exactly the two baseline tests went red (2
failed, 13 passed).
**At head `8d3b3a0eb`** (all runs under the shared verify lock):
- `pnpm exec vitest run` over the new file,
`packages/app-shell/src/console/marketplace/` (its 29 test files mock
the panel), the three Studio Access-pillar suites, `studio-locale.i18n`,
`select-placeholder-literal-11252`, all of `packages/i18n/`, and the
three i18n script tests: **120 files passed, 1812 tests passed, 13
skipped**. The skips are pre-existing and none is in the new file.
- `pnpm --filter @object-ui/app-shell --filter @object-ui/i18n run
type-check`, which is `tsc --noEmit` plus `tsc -p tsconfig.test.json`:
both `Done`. `--listFiles` on app-shell's test project lists the new
test file once.
- `packages/i18n/src/__tests__/de-quote-pairing-3876.test.ts` went red
on the first run. It pins the de census, and the five new de values add
seven matched `„…“` spans. The census is moved 77 → 84 with `rdq` 0, and
a history line is added like those before it.
**Narrowed, and declared.** The whole of `packages/app-shell/` (1,271
test files) was started at `--maxWorkers=2`. After 19 minutes it had not
finished, and a sibling's build was queued behind it. I stopped it
before it printed a result, so that run is NOT MEASURED. CI's `pnpm
test` runs the whole package. The diff changes no export and no shared
module outside the panel, its client, the two mounts' string objects and
the locale tables.
## Gates
At `8d3b3a0eb`, each exit 0: `check-i18n-call-site-keys`,
`check-i18n-en-drift`, `check-i18n-designer-table-parity`,
`check-control-bytes`, `check-new-cross-file-line-citations` (0 new),
`check-changeset-presence` (17 source files of 2 released packages, 1
changeset), `check-changeset-no-major`, `check-vi-mock-override-shape`,
`check-test-path-roots`. `check-i18n-dead-keys` is report-only and names
none of the new keys. A control-byte self-scan over the diff found
nothing.
**ESLint** (`--no-inline-config`, `--format json`) over the 17 touched
TS files reports 1 error and 29 warnings. The base file under the same
flags reads the same: `StudioDesignSurface.tsx` 1 error and 14 warnings,
`MarketplacePackagePage.tsx` 0 and 11, `suggestedBindingsApi.ts` 0 and
4. The error is the pre-existing `react-hooks/static-components` finding
in that file, far from this diff. The panel goes from 1 warning to 0.
The population is each package's own `eslint .` under the root
`eslint.config.js`. That config enables no type-aware linting, so this
diff cannot change a verdict on an untouched file. The repo-wide `pnpm
lint` is CI's.
**NOT MEASURED locally**: `check-eager-closure-budget` and
`check-eager-locale-catalogues`, which need a full console build and are
CI's (`performance-budget.yml`). The `en` pack grows by seven short
values. A browser drive of the panel is also not measured: a stock
showcase has no pending suggestion for the panel to draw, and a
vanilla-plugin boot needs an objectstack configuration change this card
does not make. The wire was measured instead, above.
## Changeset
`.changeset/7611-suggested-binding-states.md`: `@object-ui/app-shell`
minor (a new user-visible action) and `@object-ui/i18n` minor.
**Clause-②: yes (widening)**: seven locale keys are added to the
published packs (`marketplace.suggestedBindings.guestUnsupported`,
`acceptedState`, `notInForceState`, `dismissedState`, `revoke`,
`revoking`, `revokedToast`), which widens `TranslationKeys`. No export,
prop or accept set moves. The panel and its client are not exported from
`@object-ui/app-shell`. The Studio Access pillar's own table gains the
matching seven `engine.studio.access.suggest…` keys in English and
Chinese.
No doc names this panel: a grep of `content/` and every `*.md` outside
changesets and changelogs finds nothing, so no doc is updated.
## Acceptance notes (out of scope; carrier: none named)
- **The list cannot say whether an accepted set is in force.** An
accepted row reads the same whether `everyone` carries the set or the
derivation withholds it. A withhold happens when a later publish adds a
forbidden bit, or the catalog stops serving the set. So the panel's
accepted line claims the decision only, and "not yet in force" survives
only until the panel re-reads. Closing that would take a server field on
the list row. That is an objectstack contract question, not an objectui
fix.
- **A revoke is final on this surface.** A revoked row is `dismissed`
with a deployment decision. The reconcile never re-opens such a row, and
confirm takes `pending` only. Re-accepting a revoked set therefore has
no route here, which the measurement above shows (`409` on confirm after
a revoke). Dismissing a pending row was already final before #22854.
- A revoked row and a declined row are the same on the wire, so after a
re-read the panel says "dismissed or revoked" for both.
## Size
Against base `158c75d` (`git diff --shortstat 158c75d 8d3b3a0`): 18
files, 667 additions and 79 deletions, so 746 changed lines. The new
test file is 288 of them, and the ten locale packs are 70.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Rerax7QTjKMPCUZxQUtPFR)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a7d4920 commit f962cdc
18 files changed
Lines changed: 667 additions & 79 deletions
File tree
- .changeset
- packages
- app-shell/src
- components
- console/marketplace
- services
- views
- metadata-admin
- studio-design
- i18n/src
- __tests__
- locales
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
0 commit comments