Skip to content

feat(desktop): unified agentWorkingSignal for all working indicators#1494

Merged
tellaho merged 3 commits into
tho/observer-owner-globalfrom
tho/agent-working-signal
Jul 7, 2026
Merged

feat(desktop): unified agentWorkingSignal for all working indicators#1494
tellaho merged 3 commits into
tho/observer-owner-globalfrom
tho/agent-working-signal

Conversation

@tellaho

@tellaho tellaho commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1493 (tho/observer-owner-global), which stacks on #1492. Merge order: #1492#1493 → this.

Overview

Category: improvement
User Impact: "Agent is working" indicators (left-nav channel badges, composer activity bar, profile badges, activity panel, stop-turn) now agree with each other and appear reliably, instead of some surfaces showing an agent as working while others show nothing.

Problem: The app had two independent "working" signals: typing indicators (kind 20002, 8s TTL) drove the composer bar and the activity panel's stop-turn state, while observer-derived active turns (kind 24200) drove sidebar working badges and profile badges. Depending on build, harness, and timing, one pipe could be live while the other was silent — so a user could clearly see an agent working in chat while the sidebar badge, activity panel, or composer bar disagreed.

Solution: One reusable signal module, agentWorkingSignal, becomes the single source of truth: observer-derived turns are primary (they carry channel scope and a start anchor), bot typing is the fallback when no observer data exists for that scope. It supports both the channel-scoped rule and the all-channels rule, exposes reference-stable hooks, and is deliberately generalized so future surfaces (e.g. thread ingresses) consume the same hooks. Every current working-affordance surface is migrated onto it.

File changes

desktop/src/features/agents/agentWorkingSignal.ts
New module: typing registry (fed per-channel by the typing hooks), observer-primary merge logic, cached snapshot selectors, and useAgentWorking / useWorkingChannels / useChannelWorkingAgentPubkeys hooks.

desktop/src/features/agents/agentWorkingSignal.test.mjs
15 tests: scope rules, typing fallback, observer-over-typing precedence, TTL clearing, anchor stability across re-reports, channel merging, snapshot reference stability, notify dedupe, workspace reset.

desktop/src/features/channels/ui/useChannelActivityTyping.ts
Mirrors botTypingEntries into the signal via reportChannelBotTyping; entries follow the typing TTL because the effect re-reports on every change and clears on unmount.

desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts
Sidebar working badges read useWorkingChannels() — badges now also appear for typing-only agents (previously invisible in the nav when observer data was absent).

desktop/src/features/channels/ui/ChannelPane.tsx
Composer activity bar reads the unified per-channel working set instead of typing-only pubkeys; drops the isWorking prop drilling into the session panel.

desktop/src/features/channels/ui/BotActivityBar.tsx
Prop renamed typingBotPubkeysworkingBotPubkeys; internals renamed to match — the bar now represents working agents, not just typing ones.

desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
Computes isWorking internally via useAgentWorking(agent.pubkey, channel?.id ?? null) — the panel's working/stop-turn state now clears on turn_completed instead of waiting out the typing TTL.

desktop/src/features/agents/ui/ManagedAgentRow.tsx, desktop/src/features/profile/ui/UserProfilePanelSections.tsx, desktop/src/features/profile/ui/UserProfilePopover.tsx
"Working in #channel" badges read useAgentWorking(...).channels (observer channels plus typing-only channels, deduped observer-first).

desktop/src/features/workspaces/useWorkspaceInit.ts
resetAgentWorkingSignal() added to resetWorkspaceState() per the workspace-switching singleton contract.

Reproduction Steps

  1. Run the desktop app with a managed agent and give it a task in a channel.
  2. Watch the sidebar working badge, composer activity bar, and activity panel header — all three now show working together and clear together when the turn completes.
  3. To see the fallback direction: with an agent whose observer stream is absent (e.g. a harness without relay observer), the sidebar badge and activity panel still show working from the typing signal — previously only the composer bar did.
  4. To see the primary direction: with an agent whose typing signal is absent, the composer bar still shows the agent working from observer turns — previously it stayed empty.
  5. Automated: cd desktop && pnpm test (1573/1573, 15 new).

Note: ModelPicker intentionally stays on raw useActiveAgentTurns — it needs actual observer turn targets for control frames, not a display signal.

@tellaho tellaho force-pushed the tho/observer-owner-global branch from c44c50b to a7c6b39 Compare July 6, 2026 16:51
tellaho added a commit that referenced this pull request Jul 6, 2026
Rebased from tho/agent-working-signal (#1494) onto post-#1380 main.

One signal module becomes the single source of truth for 'agent is
working': observer-derived turns primary (channel scope + start anchor),
bot typing fallback when no observer data exists for the scope.

- agentWorkingSignal.ts: typing registry, observer-primary merge,
  cached snapshot selectors, useAgentWorking / useWorkingChannels /
  useChannelWorkingAgentPubkeys hooks (15 tests)
- Sidebar badges, composer activity bar, profile badges/popover, and
  the activity panel's stop-turn state all migrate onto it
- resetAgentWorkingSignal() wired into workspace reset

Conflict notes vs #1380 (its behavior preserved):
- AgentSessionThreadPanel keeps #1380's channelId prop; the unified
  isWorking now scopes to sessionChannelId (channelId ?? channel.id)
  so the explicit channel scope also drives the working state
- ChannelPane keeps passing channelId; the typing-only isWorking prop
  is replaced by the panel's internal unified signal
- Profile sections/popover keep #1380's layout; activeTurns now reads
  useAgentWorking(...).channels (superset: observer + typing-only)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/agent-working-signal branch from 941db92 to f901cf0 Compare July 6, 2026 16:51
@tellaho tellaho force-pushed the tho/observer-owner-global branch from a7c6b39 to be5994c Compare July 6, 2026 17:09
tellaho added a commit that referenced this pull request Jul 6, 2026
Rebased from tho/agent-working-signal (#1494) onto post-#1380 main.

One signal module becomes the single source of truth for 'agent is
working': observer-derived turns primary (channel scope + start anchor),
bot typing fallback when no observer data exists for the scope.

- agentWorkingSignal.ts: typing registry, observer-primary merge,
  cached snapshot selectors, useAgentWorking / useWorkingChannels /
  useChannelWorkingAgentPubkeys hooks (15 tests)
- Sidebar badges, composer activity bar, profile badges/popover, and
  the activity panel's stop-turn state all migrate onto it
- resetAgentWorkingSignal() wired into workspace reset

Conflict notes vs #1380 (its behavior preserved):
- AgentSessionThreadPanel keeps #1380's channelId prop; the unified
  isWorking now scopes to sessionChannelId (channelId ?? channel.id)
  so the explicit channel scope also drives the working state
- ChannelPane keeps passing channelId; the typing-only isWorking prop
  is replaced by the panel's internal unified signal
- Profile sections/popover keep #1380's layout; activeTurns now reads
  useAgentWorking(...).channels (superset: observer + typing-only)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/agent-working-signal branch from f901cf0 to 8ca0930 Compare July 6, 2026 17:09
Rebased from tho/agent-working-signal (#1494) onto post-#1380 main.

One signal module becomes the single source of truth for 'agent is
working': observer-derived turns primary (channel scope + start anchor),
bot typing fallback when no observer data exists for the scope.

- agentWorkingSignal.ts: typing registry, observer-primary merge,
  cached snapshot selectors, useAgentWorking / useWorkingChannels /
  useChannelWorkingAgentPubkeys hooks (15 tests)
- Sidebar badges, composer activity bar, profile badges/popover, and
  the activity panel's stop-turn state all migrate onto it
- resetAgentWorkingSignal() wired into workspace reset

Conflict notes vs #1380 (its behavior preserved):
- AgentSessionThreadPanel keeps #1380's channelId prop; the unified
  isWorking now scopes to sessionChannelId (channelId ?? channel.id)
  so the explicit channel scope also drives the working state
- ChannelPane keeps passing channelId; the typing-only isWorking prop
  is replaced by the panel's internal unified signal
- Profile sections/popover keep #1380's layout; activeTurns now reads
  useAgentWorking(...).channels (superset: observer + typing-only)

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/observer-owner-global branch from be5994c to ec29b2e Compare July 6, 2026 18:08
@tellaho tellaho force-pushed the tho/agent-working-signal branch from 8ca0930 to 8110f35 Compare July 6, 2026 18:08
@tellaho tellaho requested a review from wpfleger96 July 6, 2026 19:49
@tellaho tellaho marked this pull request as ready for review July 6, 2026 19:49
// agentWorkingSignal (sidebar badges, activity panel, composer bar) get the
// typing fallback. Entries follow the typing TTL because this effect
// re-reports whenever botTypingEntries changes.
const botTypingPubkeyKey = botTypingEntries

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [IMPORTANT · correctness] Thread-only bot typing marks the whole channel as working.

This new reportChannelBotTyping mirror effect builds botTypingPubkeyKey from the unfiltered botTypingEntries. The loop above (lines 82–89) splits agent-vs-human only — it never distinguishes thread-scoped from channel-scoped entries. ChannelPane.tsx:332 then reads this back via useChannelWorkingAgentPubkeys(activeChannel?.id) to drive the main composer activity bar.

Result: an agent typing only in an open thread lights the main channel composer bar and feeds false channel-level activity into sidebar/profile surfaces. Note the sibling thread path (ChannelPane.tsx threadComposerBotTypingPubkeys) correctly applies a threadHeadId filter — this new channel mirror was added without the equivalent scope guard.

Suggested fix — filter to channel-scoped entries before building the key:

const botTypingPubkeyKey = botTypingEntries
  .filter((entry) => entry.threadHeadId === null)
  .map((entry) => entry.pubkey.toLowerCase())
  .sort()
  .join(",");

Plus a regression test: "thread-only bot typing does not mark channel-level working."

Everything else here is solid — the observer-primary / typing-fallback merge is well-factored and covered by focused tests, and I found no subscription/memory leak across the 8 consumers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated by Ned 🤖 in f4456ff.

The reportChannelBotTyping mirror effect built its pubkey key from all
bot typing entries, so an agent typing only inside a thread lit the
main composer activity bar and fed false channel-level working state
into sidebar/profile surfaces.

Extract channelScopedBotTypingPubkeyKey, which keeps only
threadHeadId === null entries (thread surfaces apply their own
threadHeadId filter), and use it in the mirror effect. Regression
tests cover the helper and the end-to-end signal path: thread-only
typing no longer marks channel-level working; channel-scoped typing
still does.

Addresses review feedback on #1494.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
…1495)

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
@tellaho tellaho merged commit 35d43a9 into tho/observer-owner-global Jul 7, 2026
18 checks passed
@tellaho tellaho deleted the tho/agent-working-signal branch July 7, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants