feat: unfurl dashboard links shared in Slack#4244
Merged
Merged
Conversation
Slack only renders an inline logo for auth-gated pages when the app that owns the domain answers link_shared with chat.unfurl (same mechanism as Linear's link previews). Register the dashboard hostname as an unfurl domain in the generated Slack app manifest (with links:write), and have the trigger webhook respond to link_shared by unfurling every shared dashboard link with the Gram favicon and a title humanized purely from the URL path — no database lookups, so nothing leaks beyond what the pasted URL already shows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VszAzsDK7sBjGW1GeNPNZX
🦋 Changeset detectedLatest commit: 1dc3427 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The unfurl icon already pointed at the dashboard favicon (the Speakeasy stacked-layers mark); align the labels with it — alt text, the empty-path fallback title, and code comments now say Speakeasy instead of Gram. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VszAzsDK7sBjGW1GeNPNZX
Collaborator
🚀 Preview Environment (PR #4244)Preview URL: https://pr-4244.dev.getgram.ai
Gram Preview Bot |
Contributor
There was a problem hiding this comment.
1 issue found across 13 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/internal/background/triggers/app.go">
<violation number="1" location="server/internal/background/triggers/app.go:754">
P2: Paused Slack triggers still create link previews because this call runs before `ProcessEvent` applies the active-status check. Gate the best-effort unfurl on `StatusActive` so pausing a trigger disables all webhook effects.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Add NewSlackClientWithBaseURL so tests can point the Slack client at a fake API server, then drive unfurlSlackGramLinks with a real link_shared webhook body and assert the chat.unfurl request: bearer token, unfurl_id addressing, foreign-domain filtering, and the favicon + humanized-title context block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VszAzsDK7sBjGW1GeNPNZX
Gate unfurling on trigger StatusActive — it runs before ProcessEvent, so paused triggers would otherwise keep producing link previews. Also drop separator-only path segments (e.g. "--") whose humanized form is empty so they can't yield a blank unfurl label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VszAzsDK7sBjGW1GeNPNZX
alx-xo
approved these changes
Jul 15, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 16, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 16, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Jul 16, 2026
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Summary
Gram dashboard links pasted in Slack currently render bare — the dashboard is an auth-gated SPA, so Slack's crawler can never show a meaningful icon or title. This PR gives them Linear-style link previews: the Gram logo plus a humanized page title, supplied by the Gram Slack app itself.
Manifest (dashboard):
unfurl_domainsentry (gated on a webhook being configured, skipped for dot-less hosts likelocalhost), which is what makes Slack deliverlink_sharedevents for our links.link_sharedevent binding now also grantslinks:write, required bychat.unfurl.Server (trigger webhook):
decodeSlackEventgains alink_sharedbranch (message_ts→Ts,thread_ts, channel) so correlation works for these events.unfurlSlackGramLinkson the triggersApp, called at webhook ingress beforeProcessEvent— link previews render even when no assistant subscribes tolink_shared. It mirrors the best-effortackSlackThreadStatuspattern: bot token from the trigger environment, failures only cost the preview./acme/projects/default/toolsets/my-tools→ "My Tools · Toolsets"; opaque UUID/numeric segments are skipped). No database lookups, so an unfurl reveals nothing beyond what the pasted URL already shows.SlackClient.Unfurlwrapper forchat.unfurl, preferringunfurl_id+source(composer support) overchannel+ts.ProcessWebhook) only runs in the HTTP server.Notes
links:write/unfurl_domains.Testing
mise build:server,mise lint:server,pnpm -F dashboard lint, dashboardtsc -p tsconfig.app.json --noEmitall pass.link_shareddecode, title derivation, slug humanization, mrkdwn escaping.go test ./internal/background/triggers ./internal/triggerspass.🤖 Generated with Claude Code
https://claude.ai/code/session_01VszAzsDK7sBjGW1GeNPNZX
Summary by cubic
Unfurl Gram dashboard links shared in Slack with the Speakeasy logo (dashboard favicon) and a humanized page title. Link previews are sent via
chat.unfurlat webhook ingress and only when the Slack trigger is active.New Features
links:writeand setunfurl_domainsto the dashboard hostname when a webhook is configured (skips dotless hosts likelocalhost).link_sharedand unfurl dashboard links before event processing; runs only for active triggers; titles come from URL paths (drops opaque IDs and separator-only segments); Speakeasy branding with a safe fallback title ("Speakeasy dashboard"); supports bothunfurl_id/sourceandchannel/ts.Unfurlhelper andNewSlackClientWithBaseURLfor end-to-end tests against a fake Slack API; tests coverchat.unfurl, composer addressing, and foreign-domain filtering.Migration
links:writeand register the dashboard host.SLACK_BOT_TOKENis set for the trigger environment.Written for commit 4966893. Summary will update on new commits.