Skip to content

Expire a peer's typing state when its stop signal never arrives #342

Description

@HMarzban

Parent

#328.

What to build

The typing bounce on a face has no keepalive and no expiry. If the stop signal is lost, the bounce stays. It clears only when another presence event rewrites that person, and nothing bounds that time. The emoji and voice activity chips already solve this with a resend and an expiry. Typing should follow the same rule, so a stale bounce clears within seconds.

Acceptance criteria

  • While a person keeps typing, their client resends the typing start at most once every 3 s.
  • A peer drops the typing bounce 8 s after the last typing start it received, if no stop arrived.
  • The expiry never overwrites a newer state. A face that went OFFLINE or ONLINE in between stays as it is.
  • A normal stop still clears the bounce at once, as today: 1 s after the last key, or on send.
  • The typing bounce still hands over to an emoji or voice chip as today.
  • The existing sender test file has one new case for the resend, and it passes.
  • The chatroom CLAUDE.md describes the typing resend and expiry next to the "Activity chips" bullet.

Blocked by

None — can start now.

Agent brief

Type: AFK — an agent can finish this alone.

Category: bug

Current behavior:

  • The sender sends startTyping once per burst, guarded by hasStartedTyping (apps/webapp/src/components/chatroom/components/MessageComposer/helpers/handleTypingIndicator.ts:50-58).
  • It sends stopTyping 1 s after the last key (:45-48), or at once on send (:61-64).
  • sendTypingIndicator ignores the send result (:35-42). It also sends nothing when broadcaster or profile is missing (:25-27). A lost stop is never resent.
  • The peer sets TYPING on startTyping and ONLINE on stopTyping, with no timer (apps/webapp/src/hooks/useBroadcastListener.ts:35-38).
  • Activity chips differ. The sender resends every 3 s (handleTypingIndicator.ts:15, :87-89). The peer expires the chip 8 s after the last start (useBroadcastListener.ts:6, :39-50).
  • Presence events rewrite the status. A presence sync sets every tracked face to ONLINE (apps/webapp/src/services/workspacePresenceSync.ts:78-89, :255-257). A join sets ONLINE (:258-266) and a leave sets OFFLINE (:267-271).
  • AvatarStack shows the bounce when status === 'TYPING' (apps/webapp/src/components/ui/AvatarStack.tsx:74).
  • Evidence: code-traced on 2026-09-28. Not reproduced in a browser.

Desired behavior:

  • Sender: in the StartTyping branch, when hasStartedTyping is true, resend startTyping if 3 s have passed since the last start. Keep the time of the last start in module state. Reset it wherever hasStartedTyping goes back to false. No interval is needed: typing ends 1 s after the last key.
  • Peer: keep a separate expiry map for typing. Do not reuse activityExpiries. On startTyping, set TYPING and arm an 8 s timer. On stopTyping, clear the timer. When the timer fires, read the face from useStore.getState().usersPresence. Set ONLINE only if its status is still TYPING. Clear the map in the effect cleanup.
  • A presence sync still resets the face to ONLINE. The next resend brings the bounce back within 3 s.
  • A client from before this change never resends. Its bounce drops after 8 s of steady typing, then returns on its next burst. Accept this.
  • Use the same 3 s and 8 s values as the activity chips. 8 s stays above two resends, so one lost resend keeps the bounce.

Where to start:

  • handleTypingIndicator.ts: handleTypingIndicator, debouncedStopTypingIndicator, startComposerActivity.
  • useBroadcastListener.ts: the typingIndicator handler and the effect cleanup.
  • handleTypingIndicator.test.ts beside the sender: it already uses fake timers and a fresh module per test.

Line numbers are hints as of 2026-09-28; the agent searches by symbol.

Rules that apply:

  • apps/webapp/src/components/chatroom/CLAUDE.md §MessageComposer, the "Activity chips" bullet.
  • AGENTS.md §Test Policy, case (c): this is a timing rule that is hard to check by hand. Add one case to the existing sender test file. Add no test for the peer.
  • AGENTS.md §Documentation And Comments: comments are 4 lines at most, and say why.
  • The CLAUDE.md edit is prose. Write it in Simplified English: .cursor/skills/tech-writer/SKILL.md §Simplified English (house standard).

Verify:

  1. bun run --filter @docs.plus/webapp test handleTypingIndicator passes.
  2. Prove it by sabotage. Remove the resend, and the new case must fail. Put the resend back.
  3. bun run --filter @docs.plus/webapp typecheck passes.
  4. bun run lint and bun run check:agent-docs pass.
  5. Open one pad in two browsers, signed in as two members, with the same heading chat open. Check light and dark, desktop and phone.
  6. Type for 10 s without a pause in browser A. Browser B keeps the bounce the whole time, in the chat participants stack and on the TOC row.
  7. Stop typing in A. B clears the bounce about 1 s later.
  8. Open DevTools in A. In its console, run setTimeout(() => { debugger }, 2000). Type without a break until the pause hits. Keep A paused. The 1 s stop is now frozen.
  9. B drops the bounce about 8 s later. Before this change, B keeps it well past 8 s, until A's socket times out and B gets a leave (unverified).

Out of scope

  • Any change to the activity chips, their timings, or Profile.activity.
  • Presence tracking, the heartbeat, and the status column in Postgres.
  • The look of the typing bounce.

Activity

  1. added
    bugSomething isn't working
    ChatRelated to chat features
    on Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ChatRelated to chat featuresbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions