Skip to content

feat: unfurl dashboard links shared in Slack#4244

Merged
simplesagar merged 4 commits into
mainfrom
worktree-slack-link-unfurls
Jul 17, 2026
Merged

feat: unfurl dashboard links shared in Slack#4244
simplesagar merged 4 commits into
mainfrom
worktree-slack-link-unfurls

Conversation

@simplesagar

@simplesagar simplesagar commented Jul 15, 2026

Copy link
Copy Markdown
Member

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):

  • The generated Slack app manifest registers the dashboard hostname as an unfurl_domains entry (gated on a webhook being configured, skipped for dot-less hosts like localhost), which is what makes Slack deliver link_shared events for our links.
  • The link_shared event binding now also grants links:write, required by chat.unfurl.

Server (trigger webhook):

  • decodeSlackEvent gains a link_shared branch (message_tsTs, thread_ts, channel) so correlation works for these events.
  • New unfurlSlackGramLinks on the triggers App, called at webhook ingress before ProcessEvent — link previews render even when no assistant subscribes to link_shared. It mirrors the best-effort ackSlackThreadStatus pattern: bot token from the trigger environment, failures only cost the preview.
  • Titles are derived purely from the URL path (/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.
  • New SlackClient.Unfurl wrapper for chat.unfurl, preferring unfurl_id+source (composer support) over channel+ts.
  • The worker passes a nil site URL — webhook ingress (ProcessWebhook) only runs in the HTTP server.

Notes

  • Unfurls only appear in workspaces where a Gram Slack app (created from the updated manifest) is installed — same constraint as Linear. Existing installs need the app manifest updated + reinstalled to pick up links:write/unfurl_domains.

Testing

  • mise build:server, mise lint:server, pnpm -F dashboard lint, dashboard tsc -p tsconfig.app.json --noEmit all pass.
  • New unit tests: link_shared decode, title derivation, slug humanization, mrkdwn escaping.
  • go test ./internal/background/triggers ./internal/triggers pass.

🤖 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.unfurl at webhook ingress and only when the Slack trigger is active.

  • New Features

    • Dashboard manifest: add links:write and set unfurl_domains to the dashboard hostname when a webhook is configured (skips dotless hosts like localhost).
    • Trigger webhook: decode link_shared and 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 both unfurl_id/source and channel/ts.
    • Slack client: add Unfurl helper and NewSlackClientWithBaseURL for end-to-end tests against a fake Slack API; tests cover chat.unfurl, composer addressing, and foreign-domain filtering.
  • Migration

    • Update and reinstall the Slack app from the new manifest to grant links:write and register the dashboard host.
    • Ensure SLACK_BOT_TOKEN is set for the trigger environment.

Written for commit 4966893. Summary will update on new commits.

Review in cubic

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-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1dc3427

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
server Patch
dashboard Patch

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
@simplesagar
simplesagar marked this pull request as ready for review July 15, 2026 20:01
@simplesagar
simplesagar requested review from a team as code owners July 15, 2026 20:01
@simplesagar simplesagar added the preview Spawn a preview environment label Jul 15, 2026
@speakeasybot

speakeasybot commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

🚀 Preview Environment (PR #4244)

Preview URL: https://pr-4244.dev.getgram.ai

Component Status Details Updated (UTC)
✅ Database Ready Existing database reused 2026-07-16 15:11:27.
✅ Images Available Container images ready 2026-07-16 15:11:11.

Gram Preview Bot

@simplesagar simplesagar changed the title feat(triggers): unfurl Gram dashboard links shared in Slack feat(triggers): unfurl dashboard links shared in Slack Jul 15, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread server/internal/background/triggers/app.go
Comment thread server/internal/background/triggers/slack_unfurl.go Outdated
@simplesagar simplesagar changed the title feat(triggers): unfurl dashboard links shared in Slack feat: unfurl dashboard links shared in Slack Jul 15, 2026
simplesagar and others added 2 commits July 15, 2026 13:30
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
@simplesagar
simplesagar added this pull request to the merge queue Jul 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 16, 2026
@simplesagar
simplesagar added this pull request to the merge queue Jul 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 16, 2026
@simplesagar
simplesagar added this pull request to the merge queue Jul 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 16, 2026
@simplesagar
simplesagar added this pull request to the merge queue Jul 17, 2026
Merged via the queue into main with commit f96b6fb Jul 17, 2026
38 checks passed
@simplesagar
simplesagar deleted the worktree-slack-link-unfurls branch July 17, 2026 00:21
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

preview Spawn a preview environment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants