Skip to content

Adapters should have an opportunity to handle results #306

@heyitsaamir

Description

@heyitsaamir

Problem Statement

I'm not sure if the title of this issue is the best, but let me describe the issue first.

In Teams, "actions" are not async. So if you have a modal you want to open, you get a request, and you respond to it with the parameters of the modal, and then it opens. This differs from Slack, where you can cache the parameters for modal opening, and then imparatively trigger the modal to open asynchronously.

With the current Chat interface, processAction returns void. So when the handler gets an open-modal event from Teams, it calls chat.processAction, but it can't do anything with the result of that. The expectation is that chat will call openModal which then imparatively should open the modal.

Proposed Solution

There's a few potential ways to handle this imo:

  1. Allow processAction to return the results of actions. So processAction could return the result of openModal, and then it is the responsibility of the adapter to handle that (cache it, send it imparatively, return it immediately. It's on the adapter).
  2. Make processAction awaitable. If processAction is awaitable, then openModal can resolve the promise once it's called. This option seems fragile to me, but it's an option.
  3. Pass openModal inline as part of processAction. Then in the openModal interceptor, the options.openModal is called first (if provided). The adapter can listen to this function resolving (maybe via a Promise.race).
processAction(event, {
    waitUntil,
    onOpenModal: async (modal) => {
      // adapter-specific handling right here
      // Teams: stash it for the invoke response
      // Slack: call views.open
      return { viewId: '...' };
    }
  });

Alternatives Considered

No response

Use Case

Here is a more full-example of the 3rd option:


 app.on('dialog.open', async (ctx) => {
    const activity = ctx.activity;
    const threadId = this.encodeThreadId({
      conversationId: activity.conversation?.id || "",
      serviceUrl: activity.serviceUrl || "",
    });

    // Promise that resolves when the handler calls openModal
    let resolveModal: (modal: ModalElement) => void;
    const modalPromise = new Promise<ModalElement>((resolve) => {
      resolveModal = resolve;
    });
    let modalWasOpened = false;

    const actionEvent = {
      actionId: activity.value?.data?.actionId || "dialog.open",
      value: activity.value?.data,
      triggerId: activity.id,
      user: { /* ... from activity */ },
      messageId: activity.replyToId || activity.id || "",
      threadId,
      adapter: this,
      raw: activity,
    };

    // processAction is still fire-and-forget, but the interceptor
    // captures the modal synchronously during handler execution
    this.chat.processAction(actionEvent, {
      waitUntil: (p) => p, // no-op, we handle timing ourselves
      onOpenModal: async (modal, contextId) => {
        modalWasOpened = true;
        resolveModal(modal);
        return { viewId: contextId };
      },
    });

    // Wait for the handler to call openModal (or timeout)
    const modal = await Promise.race([
      modalPromise,
      new Promise<null>((resolve) => setTimeout(() => resolve(null), 5000)),
    ]);

    if (modal && modalWasOpened) {
      const card = modalElementToAdaptiveCard(modal);
      return {
        task: {
          type: 'continue',
          value: {
            title: modal.title || 'Dialog',
            card: { contentType: 'application/vnd.microsoft.card.adaptive', content: card },
          },
        },
      };
    }

    // Handler didn't open a modal — close silently
    return { status: 200 };
  });

Priority

None

Contribution

  • I am willing to help implement this feature

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions