feat(desktop): unified agentWorkingSignal for all working indicators#1494
Conversation
c44c50b to
a7c6b39
Compare
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>
941db92 to
f901cf0
Compare
a7c6b39 to
be5994c
Compare
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>
f901cf0 to
8ca0930
Compare
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>
be5994c to
ec29b2e
Compare
8ca0930 to
8110f35
Compare
| // 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 |
There was a problem hiding this comment.
🟡 [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.
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>
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/useChannelWorkingAgentPubkeyshooks.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
botTypingEntriesinto the signal viareportChannelBotTyping; 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
isWorkingprop drilling into the session panel.desktop/src/features/channels/ui/BotActivityBar.tsx
Prop renamed
typingBotPubkeys→workingBotPubkeys; internals renamed to match — the bar now represents working agents, not just typing ones.desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx
Computes
isWorkinginternally viauseAgentWorking(agent.pubkey, channel?.id ?? null)— the panel's working/stop-turn state now clears onturn_completedinstead 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 toresetWorkspaceState()per the workspace-switching singleton contract.Reproduction Steps
cd desktop && pnpm test(1573/1573, 15 new).Note:
ModelPickerintentionally stays on rawuseActiveAgentTurns— it needs actual observer turn targets for control frames, not a display signal.