Conversation
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>
|
@heyitsaamir is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
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>
bensabic
left a comment
There was a problem hiding this comment.
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!
Summary
Test plan
Screen.Recording.2026-04-02.at.10.26.46.PM.mov
Tested manually (And tests work).
Notes:
openDialogis called, then we resolve the call with no response.