feat(api): surface describe_feature related_surfaces on ChatResponse - #187
Merged
Conversation
The describe_feature tool returns `related_surfaces` in its read-only payload, but BuildActionResult returns null for read-only tools so it never reached the client. Extract `related_surfaces` from successful read-only tool payloads in the tool-execution accumulator (deduped, first-seen order) and thread it onto a new optional `ChatResponse.RelatedSurfaces`. The client maps these app-surface IDs to deep links in a "Related" chat footer. Refs thomasluizon/orbit-ui-mobile#95 Paired UI PR (consumes RelatedSurfaces, renders the footer): thomasluizon/orbit-ui-mobile feature/wave2-chat-rendering Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Threads related_surfaces from read-only tool payloads through to ChatResponse.RelatedSurfaces cleanly. The extraction logic correctly handles the JSON round-trip, guards against non-object/missing-array payloads, and deduplicates in first-seen order. The null-when-empty sentinel keeps older clients unaffected. Unit tests cover both the happy-path (surfaces populated) and the negative case (mutating-only turn leaves the field null). No correctness, security, contract, or hard-rule issues found.
thomasluizon
added a commit
that referenced
this pull request
Jun 27, 2026
…ing (#261) * feat(gamification): free-tier rebalance + infinite levels + recap (#186, #190) Reversible gamification_free_tier AppFeatureFlag (default off) unlocks streak/XP/level + streak-freeze auto-activation for free users. Infinite levels via 100*L^2 curve, continuous past level 10. New half_year_hero/streak_titan achievements + reserved first_cheer. GET /api/gamification/recap returns metrics-only retrospective + referral shareDeepLink, ungated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(astra): warm persona tone + soften act-immediately framing (#183) New EncouragingToneSection (Order 150) + softened CoreIdentitySection so clarify-first coexists with direct action; destructive/bulk route through existing confirmation-card gating. Reuses shipped resolve + pendingOperations infra; gpt-4.1-mini unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(social): social foundation - friendships, cheers, feed, moderation, handles (#193) 5 entities + User.Handle/SocialOptIn + migration w/ deterministic handle backfill. Endpoints (request/accept/remove/list/feed/cheer/block/report + set-handle + opt-in), SocialAccessGuard, rate limits. OpenAI moderation (fail-open on outage). FriendFeedEvent write-pipeline: streak hook (all users) + achievement hook (Pro) + keyset read. first_cheer wired; FCM pushes; export + deletion purge. Backend + shared only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(onboarding): setup-checklist flags, bulk-create tags, template-pack support (#187, #189) User onboarding-checklist completion flags (ride profile payload, auto-set from signals) + onboarding achievement hook (#189). BulkCreateHabits accepts per-item tags resolved/created by name for starter packs (#187). Migration AddOnboardingChecklistFlags. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): address PR #261 review — block-filter cheers, unblock validator, feed leak, period 400 (#193, #190) Resolves claude-review findings: add UnblockUserCommandValidator (HIGH); filter blocked users from GetCheersQuery both directions (HIGH); drop opted-out actors from the friend feed (MED); MaximumLength on friend-request handle/referralCode (MED); recap period set now matches the resolver via a shared IsKnownPeriod (single source of truth) + an invalid period returns 400 not 500 at the recap/retrospective endpoints (MED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(api): #261 round-2 review — block/unblock rate limits, GetFriends validator, ordering, cheer-ownership Rate-limit block/unblock (50/24h); add GetFriendsQueryValidator (UserId NotEmpty); check target existence before BlockedUser.Create; verify a reported CheerId involves the reported user (else CheerNotFound). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The
describe_featurechat tool returnsrelated_surfacesin its read-onlyToolResult.Payload(e.g.["gamification", "today"]), and the executor preserves it onAgentOperationResult.Payload. ButBuildActionResultreturnsnullfor read-only tools, so the surfaces were dropped before reaching the client — the assistant only re-emitted the markdown body as text.This threads them through:
IReadOnlyList<string>? RelatedSurfacestoChatResponse.ToolExecutionAccumulator, collectrelated_surfacesfrom successful read-only tool payloads (round-tripped throughJsonSerializerto aJsonElement), deduped in first-seen order.ChatResponse(null when empty, so an older client degrades gracefully).BuildActionResultstill returnsnullfor read-only tools — the footer rides onRelatedSurfaces, not on anActionResultchip.Why
Part of Orbit Wave 2 PR-B. The UI renders these app-surface IDs as a "Related" deep-link footer in chat (web + mobile).
Tests
ProcessUserChatCommandHandlerTests: new cases assertRelatedSurfacesis populated after adescribe_feature-style read-only payload and leftnullon a mutating-only turn.dotnet build(Orbit.slnx) clean;dotnet test tests/Orbit.Application.Testsgreen (35/35 in the handler suite).Cross-repo
Refs thomasluizon/orbit-ui-mobile#95
Paired UI PR (consumes
RelatedSurfaces, renders the footer, unifies markdown rendering, adds new-tool chips): thomasluizon/orbit-ui-mobile#139🤖 Generated with Claude Code