feat(ai-chat): address field references by name (#24701)
## Root cause The metadata references are addressed differently, and only one of the two addresses can be written before the thing exists. | | address | who generates it | writable before creation | |---|---|---|---| | object | `nameSingular` | the model | yes | | field | `fieldMetadataId` | the database, at creation | no | #23573 chose this deliberately and said so: the object marker *"is name-keyed rather than id-keyed so an object the assistant only proposes to create still renders as a chip"*, while *"field and view ids must come from a tool"*. That was correct at the time — the only thing the assistant ever proposed was an object. #24331 then added the workspace-setup prompt three weeks later, whose data-model step asks for *"the custom fields to add"* — fields that by definition have no id. That created a case the addressing scheme was never designed for. With no legal `[[field:…]]` available, the model improvised the symmetric name-addressed form, which matched no pattern, fell through `findChatReferences` and reached the user as raw brackets. ## Fix **Address fields by name, the way objects are.** `[[field:objectNameSingular:fieldName:displayName]]` is now the field reference the prompt writes. The routing already pointed this way: the field settings page is `objects/:objectNamePlural/:fieldName`, so `FieldMetadataLink` was resolving the id back into those two names just to build the link. The id was a detour. `FieldMetadataLink` now accepts either address and falls back to a static chip when the field is not there yet, exactly like `ObjectMetadataLink`. The id pattern stays as a reader so the messages already stored with it keep their chips; the prompt no longer emits it, so it ages out. The `field` kind now carries the two names, and the id one is `legacyFieldById` — named for how the field is addressed, not for a lifecycle state. A name-addressed reference to a field that *does* exist resolves and links, which is what keeps an onboarding thread navigable when it is reread after the build. **Views keep their id, on purpose.** They are the one kind where id-keying is structural rather than incidental: `view.name` has no unique constraint, so two views on one object can share a name, and the destination carries the id in the query string (`/objects/companies?viewId=…`) rather than resolving to names. So a proposed view has the same latent gap, but nothing to address it by. In practice the onboarding flow never proposes one — *"New fields land visible on their object's index view, so no view work is needed"* — and the fallback below keeps it from ever looking like this bug did. **Degrade anything unrecognized to plain text.** `replaceMalformedChatReferencesWithDisplayName` collapses any `[[record|object|field|view:…]]` marker no pattern accepts down to its display name. It runs only on the slices *between* valid references in `getChatReferenceSegments`, so a valid reference next to a broken one is untouched, and it leaves a marker carrying no display name alone rather than surfacing a bare UUID as prose. This is a deliberate, narrow step back from #23852, which removed the old compensation machinery on the grounds that the prompt would keep model output well-formed. That held for the cosmetic cases it listed; it did not survive a step where the prompt asked for something the syntax could not express. It does not *repair* a marker into a chip the way the deleted code did — it only strips one that cannot be parsed, so the surplus-bracket and unclosed-marker behaviour #23852 settled on is unchanged. `protectChatReferencesForMarkdown` returned the original `text` on its single-string-segment fast path, which would have handed back the raw brackets; it now returns the segment. ## Test - `ChatReferenceChip.stories.tsx` — a `ProposedField` story covering both halves: a field that does not exist renders as a chip with no `href`, an existing field addressed by name links to its settings page. Verified in Chromium. - `replaceMalformedChatReferencesWithDisplayName.test.ts` — every kind, unclosed markers, unknown prefixes, missing display name - `protectChatReferencesForMarkdown.test.ts` — name-addressed references survive the markdown round-trip; an unparseable marker collapses next to a valid one - `TextWithChatReferences.test.tsx` — both field addresses route to the field chip - `workspace-setup-system-prompt.constant.spec.ts` — the prompt clauses 267 front + 167 server tests pass; both packages lint, format and typecheck clean.
E
Etienne committed
e966b5d526f0550a4ab15a90476459e28987c897
Parent: aee0c31
Committed by GitHub <noreply@github.com>
on 8/25/2026, 12:13:49 PM