Skip to content

Add modal support for Teams#325

Open
heyitsaamir wants to merge 4 commits intovercel:mainfrom
heyitsaamir:feat/teams-dialog-support
Open

Add modal support for Teams#325
heyitsaamir wants to merge 4 commits intovercel:mainfrom
heyitsaamir:feat/teams-dialog-support

Conversation

@heyitsaamir
Copy link
Copy Markdown
Contributor

@heyitsaamir heyitsaamir commented Apr 3, 2026

Summary

  • Add Teams dialog (task module) support — buttons with actionType="modal" now open interactive dialogs on Teams via the task/fetch / task/submit invoke flow
  • Introduce WebhookOptions.onOpenModal hook so Teams can return modal content inline within the HTTP invoke response (no triggerId needed, unlike Slack). This is based on the decision from Adapters should have an opportunity to handle results #306 .
  • Add ButtonElement.actionType field ("action" | "modal") to the core SDK, with JSX support via - This is based on the decision in Buttons should declare dialog-opening intent upfront #311
  • New modals.ts module in the Teams adapter: converts ModalElement → Adaptive Card JSON, parses dialog.submit payloads, and maps ModalResponse → TaskModuleResponse
  • Refactor cards.ts and modals.ts to use typed @microsoft/teams.cards builders instead of hand-rolled plain objects and local interfaces. This adds better type safety for Adaptive Card components.

Test plan

Screen.Recording.2026-04-02.at.10.26.46.PM.mov

Tested manually (And tests work).

Notes:

  1. We use Promise.race in dialog open so that it waits until the application layer calls openDialog. If no openDialog is called, then we resolve the call with no response.
  2. For errors, adaptive cards actually supports lots of validation inline, and there's no clean way to do validation via the server. Right now, the handlers just replace the content with the error message. But ideally, the error message should show up with the existing form. However, to make that happen, and for that experience, we would need to cache the contents of the opened-dialog. Then if the handler gets called which returns an error, the form needs to be reconstructed with a previously built form + the error being passed in. I'd argue to do this work separately if necesary to avoid this PR from getting any bigger. Alternatively, there should be some way provided in the modal itself to do some validation inline.

heyitsaamir and others added 2 commits April 2, 2026 21:55
Teams dialogs require modal content to be returned inline in the HTTP
response when a task/fetch invoke fires. This adds:

- `actionType: "modal"` on buttons to emit msteams task/fetch hint
- `onOpenModal` hook on WebhookOptions for inline modal interception
- dialog.open/dialog.submit handlers in Teams adapter with Promise.race
- Modal-to-AdaptiveCard converter (modals.ts)
- Bridge adapter sends empty body (not "{}") for dialog close responses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hand-rolled plain JSON objects and local type definitions with
typed builder classes from @microsoft/teams.cards. This gives compile-time
type safety and eliminates the local AdaptiveCard/AdaptiveCardElement/
AdaptiveCardAction interfaces.

Also fix ephemeral modal button missing actionType="modal", which
prevented the dialog from opening on Teams.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 3, 2026

@heyitsaamir is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@heyitsaamir heyitsaamir changed the title Add dialog support for Teams Add modal support for Teams Apr 3, 2026
heyitsaamir and others added 2 commits April 3, 2026 11:19
Pass the original contextId through to re-rendered modals so subsequent
submissions can still retrieve the stored thread/message/channel context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Clean up timeout timer in handleDialogOpen to prevent resource leak
- Also race on actionPromise so errors surface instead of silently timing out
- Extract buildContinueResponse helper to deduplicate update/push cases
- Use typed TextInputOptions/ChoiceSetInputOptions instead of Record<string, unknown>
- Make processSlashCommand options parameter explicit (WebhookOptions | undefined)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@bensabic bensabic left a comment

Choose a reason for hiding this comment

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

Really nice work on this! The dialog/task module integration is well thought out and the onOpenModal hook is a clean solution for the Teams invoke-response constraint.

Just a few things I wanted to flag:

1. storeModalContext is not awaited before onOpenModal
packages/chat/src/chat.ts; Small timing issue: the state write fires without await, so if processModalSubmit runs quickly (fast state backend, tests), retrieveModalContext could come back empty. Should be a straightforward fix to await it before calling onOpenModal.

2. Race condition in Promise.race
packages/adapter-teams/src/index.ts; I think there's a subtle race here: if processAction resolves before the onOpenModal callback fires, the race returns null and the dialog silently won't open even though the handler did call openModal. Also worth noting that once modalPromise wins, actionPromise keeps running fire-and-forget without error handling or waitUntil - that could cause unhandled rejections in serverless environments. Happy to chat through ideas on how to restructure this if helpful!

3. Missing changesets
Looks like we're missing changesets for @chat-adapter/teams and the chat package to cover the new public API surface (actionType, onOpenModal, ModalResponse).

4. Unsanitized interpolation in error card
packages/adapter-teams/src/modals.ts; In the "errors" branch, response.errors keys and values get interpolated straight into markdown (`**${field}**: ${msg}`). Since these can originate from user-submitted dialog data, it'd be good to either escape them or render as separate TextBlock elements to be safe.

5. onOpenModal return type could be clearer
Teams doesn't produce a real viewId; the adapter uses contextIdinternally and the returned object goes unused. Might be worth changing the return type toPromise<{ viewId: string } | void>` or adding a doc comment noting it's platform-specific, so future adapter authors don't get tripped up.

6. ButtonProps JSX interface missing disabled
packages/chat/src/jsx-runtime.ts; The ButtonElement has disabled?: boolean and it works via the function API, but the JSX ButtonProps doesn't include it. <Button disabled> silently drops the prop - easy one to add!

7. retrieveModalContext doesn't delete after read
packages/chat/src/chat.ts; The comment says "Retrieve and delete" but no delete happens. A second submit with the same contextId would pick up stale context. The 24h TTL covers us, but a delete-after-read would be the more conventional pattern.

Nice to have

8. Unit tests for modals.ts
modalToAdaptiveCard, parseDialogSubmitValues, and modalResponseToTaskModuleResponse are all pure functions, would be great candidates for unit tests and would give us nice coverage of the core of this PR.

9. Test for actionType: "modal" in cards.test.ts
The msteams.type: "task/fetch" hint is what kicks off the whole dialog flow, a quick regression test here would be valuable.

10. DIALOG_OPEN_TIMEOUT_MS is hardcoded at 5s
Not a blocker, but worth noting, a slow cold start in a serverless function could exceed this and silently return undefined. Could be nice to make it configurable down the road.

11. @ts-expect-error in bot.tsx
The type mismatch between the async void handler and ModalSubmitHandler return type should ideally be fixed at the type level rather than suppressed in the example, since that's what folks will copy from. Totally fine to tackle separately though!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants