fix(panel): scope session mute per session, not per project - #157
Merged
Conversation
Two agents in one repo shared a single mute. The persistence key ends in an integration-supplied tabId, and no integration covers Zed or a bare shell — VSCode's also stayed nil whenever VSCODE_IPC_HOOK_CLI was absent from the agent's environment. With tabId nil both sessions collapsed onto "<agent>::<projectPath>", so muting either silenced both. Key mutes off a ladder instead: tabId where an integration supplied one, else the tty, else Claude's sidecar session id. ps already reported the tty and discover() dropped it after testing it for presence. It is the only discriminator every agent has — codex exposes no pid-to-session mapping, gemini no per-session state at all, and Antigravity's RPC keys by workspace and collapses trajectories within it, so a Claude-only fix would have left the other three broken. Names keep their existing tabId key: they are also resolved from events, which carry a tab id but no tty, and widening the key would orphan every rename already on disk. Unmuting was also not the inverse of muting. isMuted fell back to the project-wide entry but toggleMuted negated the stored flag, which is false for a session holding no entry of its own — so clicking unmute on an inherited mute wrote muted: true and the session could never be cleared from its own row. toggleMuted now negates the effective state, and `muted` is tri-state so an explicit false can outrank the project entry. A missing field still decodes as no opinion, so legacy files and the muted-sessions.json migration are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Muting one session muted the others in the same repo. Two independent bugs.
1. Mute identity collapses when no integration supplies a
tabIdSessionPersistence.keyends in atabId, and drops the segment entirely when it's nil — so two sessions in one project become one key, one mute flag.tabIdis only populated by a registered integration (TerminalRegistry.integrations): iTerm2, Terminal.app, VSCode/Cursor, Warp, Ghostty. Verified against live sessions:tabIdis nil/redteaming×2ZED_TERM, noTERM_SESSION_ID, so even the generic env-var conformer can't claim it/Helix×2VSCODE_IPC_HOOK_CLI, soenrichbails andtabIdstays nilCorroborated visually: the accent bar uses the same key shape, and both colliding rows render teal —
claude::/Users/…/redteaminghashes to teal. DistincttabIds would have given teal and indigo.Fix: mutes key off a ladder —
tabId→ tty →claudeSessionID.psalready reported the tty;discover()parsed it, tested it for presence, and threw it away. It's the only discriminator every agent has:~/.codex/sessions/…/rollout-*.jsonlis date-partitioned with no pid→session mappingliveStatusByWorkspace()keys by workspace and collapses trajectories within itso a Claude-only fix would have left the other three broken.
TerminalAppIntegrationalready uses the tty as a tabId for Terminal.app, so this generalizes existing practice rather than adding a conformer per terminal.Names deliberately keep the
tabIdkey: they're also resolved from events, which carry a tab id but no tty, so widening that key would orphan every rename on disk.2. Unmuting was not the inverse of muting
isMutedfell back to the project-wide entry;toggleMutednegated the stored flag, which isfalsefor a session holding no entry of its own. So on an inherited mute, clicking unmute wrotemuted: true— double-muted — and the session could never be cleared from its own row. Reachable via the legacymuted-sessions.jsonmigration, or any mute performed from atabId-less session.toggleMutednow negates the effective state, andmutedis tri-state so an explicitfalseoutranks the project entry. A missing field still decodes as no opinion, so existing files and the migration are unaffected.Verification
make buildandmake typecheck-testspass.swift testcan't run locally (Command Line Tools only), so the 20 assertions inSessionMuteTestswere also run against the realSessionPersistence.swiftvia a standalone harness — all pass. CI runs them properly.Covered: the scope ladder and its empty-string handling; two same-project sessions muting independently for both claude and codex; toggle round-trip; unmute leaving no entry behind; a project-wide mute being inherited, cleared on one session only, surviving reload, and re-applicable; legacy entries without a
mutedfield; a rename surviving a mute round-trip.Not covered by tests: that
discover()populates the tty at runtime. Confirmed by hand instead —ps -axo pid=,etime=,tty=,args=givesttys014/ttys016for the two Zed sessions andttys009/ttys018for the two Cursor ones,??for none.Known limits, not addressed here
SessionEntry.lastSeenAtalready exists for "a future dormancy-based eviction pass"; this is the case that would justify building it.detectAgenttwice (thenodewrapper and the binary), on one tty. Pre-existing and separate — though tty keying makes the pair mute together, which is the behaviour you'd want.Manual check
make reload, then run two agents in one repo under Zed or Cursor and mute one. Only that row should show the muted glyph, and clicking it again should clear it.