Skip to content

[WebChat] Remove multi-tab input blocking — allow sending from any tab during active runs #66212

@jaruesink

Description

@jaruesink

Summary

When a chat run is active (started from one browser tab), other tabs showing the same session display a "responding in another tab" state and block input. This is unnecessary — the server already supports concurrent chat.send calls for the same session (there's no per-session concurrency gate), and streaming events already broadcast correctly to all connected WebSocket clients.

Problem

Current behavior:

  • Tab A sends a message → run starts → streaming begins
  • Tab B (same session, same browser) sees the streaming output but shows "responding in another tab" and blocks/disables input
  • User must wait for the run to complete on Tab A before sending from Tab B

Why this is wrong:

  1. No server-side gate: chat.send is keyed by idempotencyKey (run ID), not by session. Multiple concurrent runs for the same session work fine.
  2. Events already broadcast correctly: WebSocket chat events (delta/final/aborted) go to all connected clients regardless of which tab initiated the run.
  3. The streaming already works: Tab B receives and renders streaming deltas from Tab A's run. The event-driven architecture is already in place.
  4. UX is confusing: Users expect tab-agnostic behavior (like any modern chat app). Blocking input because "another tab is responding" feels like a bug, not a feature.

Evidence from Source

The blocking appears to stem from the chatRunId state being local to each tab:

  • isChatBusy() checks host.chatSending || Boolean(host.chatRunId) (app-chat.ts:47)
  • chatRunId is only set by the tab that called sendChatMessage() — passive tabs keep chatRunId = null
  • However, chatStream IS set on passive tabs via handleChatEvent() → delta handler
  • This means isBusy (props.sending || props.stream !== null || props.canAbort) evaluates to true on passive tabs because stream !== null, which may be affecting UI state
  • The send button's ?disabled only checks !props.connected || props.sending, which should be fine — but the overall "busy" visual state and queue behavior (flushChatQueue skips when busy) creates the blocking UX

The queue system also contributes: flushChatQueue() returns early when isChatBusy() is true, so queued messages on the passive tab won't drain.

Proposed Fix

The webchat should treat runs as session-scoped, not tab-scoped:

  1. Track active runs by session, not just by tab-initiated runId: When a tab receives a chat delta event for its current session (with a runId it didn't initiate), it should adopt that runId into chatRunId so it can properly track completion, enable the stop button, and release the "busy" state when the run finishes.

  2. Don't block input during streaming: The input textarea and send button should remain fully functional during an active run. If the user sends while a run is in progress, it should either:

    • Queue the message and send it after the current run completes (current queue behavior, but with working drain), OR
    • Send immediately as a concurrent run (server already supports this)
  3. Show informational state, not blocking state: Instead of blocking, show a lightweight indicator that a run is in progress (streaming indicator, stop button). The user should be able to type and send at any time.

  4. Consistent abort across tabs: Both tabs should see the stop button for the active run and be able to abort it (the server already checks ownerDeviceId/ownerConnId for abort authorization).

Related Issues

Environment

  • OpenClaw v2026.4.3
  • WebChat UI (Lit-based)
  • Multiple browser tabs on same session

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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