Skip to content

Loading indicator flickers when reportProgress fires mid-turn #445

@sentry-junior

Description

@sentry-junior

Summary

The Slack assistant loading indicator briefly disappears and reappears during long-running turns, ~1–2 minutes after the turn starts. The flash coincides with the first reportProgress call following a long tool-call gap.

Root cause

status-scheduler.ts replaces loading_messages with a single-item array on every progress update:

// status-scheduler.ts
const getLoadingMessagesForVisibleStatus = (visible) =>
  visible ? [visible] : undefined;  // always 1 item

On status.start(), Slack receives the full 10-item shuffled carousel ("Consulting the orb", "Bribing the gremlins", …). When reportProgress fires and status.update() posts loading_messages: ["Searching docs"], Slack tears down the 10-item rotation and resets to a single message — the reset is visible as a flash.

This diverges from the canonical Slack pattern: every official sample (bolt-js-assistant-template, bolt-js-support-agent) calls setStatus once at turn start with a fixed loading_messages list and never updates it mid-turn. Progress is shown via streaming, not repeated setStatus calls.

Confidence: high — reproduced via code trace; timing matches reported 1–2 minute onset (first reportProgress after a long tool call).

Secondary issues (same investigation)

  • reply-executor.ts finally block calls status.stop() unconditionally — including after return from the timeout-resume catch path — explicitly clearing the indicator before the new invocation re-establishes it.
  • slack-resume.ts calls await status.stop() immediately before postSlackApiReplyPosts(). Slack auto-clears the indicator when a reply posts; the explicit stop creates an unnecessary pre-reply gap.

Short-term fix

Stop replacing loading_messages on mid-turn updates. The update() path should leave loading_messages unchanged (keep the carousel from start()) and skip the setStatus call when only the visible text key has changed and the array is already stable. Fix the unconditional status.stop() in the finally block for the timeout-resume path with a preserveStatusForResume flag.

Long-term proposal

Migrate from repeated setStatus updates to Slack's native chatStream with task_update chunks — the Cursor-like inline progress widget. @chat-adapter/slack already supports this via adapter.stream(threadId, asyncIterable, { taskDisplayMode: 'timeline' }). Under this model:

  • setStatus is called once at turn start (no mid-turn updates, no carousel reset)
  • reportProgress calls become task_update chunks (pending → in_progress → complete) appearing inline in the reply message
  • The final reply streams via markdown_text chunks; the streamer stop() auto-clears the status

This matches the platform's intended pattern and removes the loading_messages flicker entirely.

Action taken on behalf of David Cramer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions