fix(desktop): route /yolo, /handoff and /skin through the shared session resolver - #71832
Conversation
…ion resolver 584b274 moved slash-command targeting onto `resolveTargetSessionId` so a command runs against the conversation the user is looking at. Three action handlers were left on the old bare `hint || activeSessionIdRef.current` read: `/yolo`, `/handoff` and `/skin`. From the chat composer `sessionHint` is always undefined, so the stale ref *is* the target. That skips the resolver's rung 2 route check. When the durable route names conversation B while the runtime ref still points at A -- profile swap, gateway reconnect, an in-flight resume -- the resolver resumes B but the bare read returns A. `/yolo` then writes the per-session approval bypass to A via `config.set`, so A keeps auto-approving tool calls while B still prompts for every one, and the confirmation is appended to A's transcript instead of B's. `/handoff <platform>` is worse in one respect: it ships the wrong conversation's transcript out of the app to Telegram/Discord. All three now go through `resolveExistingSessionId` -- the same ladder with the create rung disabled, so none of them can mint a backend session and the deliberate no-session behaviour survives unchanged (`/yolo` arms locally, `/skin` toasts, `/handoff` reports it cannot run).
|
Two commits landed on this exact function today, both the same class this PR closes out: #71805 (merged 06:50 UTC) bound the busy gate and The three handlers here are the remaining call sites that still ask the foreground directly — on current Still MERGEABLE/CLEAN against that tip, with 36 checks reported and zero failures. |
|
Thanks for completing the remaining action-handler conversion. The premise is confirmed on current main: The PR reuses that resolver with a null create rung, preserving the existing no-session behavior while preventing those three handlers from acting on a stale runtime id. A current-main search found these are the only remaining Automated hermes-sweeper review. |
What does this PR do?
584b27449("slash commands target the user's chat, not a new session") introducedresolveTargetSessionIdand moved the slash pipeline onto it, so a command runs against theconversation the user is actually looking at. Three action handlers were left behind on the old
idiom the new resolver's docblock names as the bug it exists to prevent:
slash.ts:547/yoloconst sid = sessionHint || activeSessionIdRef.currentslash.ts:577/handoffconst sid = sessionHint || activeSessionIdRef.currentslash.ts:629/skinconst sid = sessionHint || activeSessionIdRef.currentFrom the chat composer,
submitTextforwards an explicit target only for a queue drain or a tile(
use-prompt-actions/index.ts:524), sosessionHintisundefinedin the ordinary case and thebare ref is the value used.
That skips the resolver's rung 2 route check (
routedNeedsResume). When the durable route namesconversation B while the runtime ref still points at A — profile swap, gateway reconnect,
an in-flight resume — the resolver resumes B, but the bare read returns A.
User-visible symptom. You stream in chat A, click chat B in the sidebar, type
/yoloin B.The per-session approval bypass is written to A (
config.set { key: 'yolo', session_id },lib/yolo-session.ts:15). B keeps prompting for every tool approval, so you press/yoloagain —toggling A back off — while A (possibly a background agent) spent that window auto-approving tool
calls. The confirmation line is appended to A's transcript, so B shows nothing at all.
/handoff <platform>has the same shape and is worse in one respect: the wrong conversation'stranscript leaves the app for Telegram/Discord.
All three now resolve through a
resolveExistingSessionIdhelper defined next toensureSessionId— the same ladder with
createSession: async () => null, so these verbs can never mint a backendsession and the deliberate no-session behaviour survives byte-for-byte (
/yoloarms locally,/skintoasts instead of spinning up a session just to change a theme,/handoffreports itcannot run). All three already branch on
!sid, so thenullreturn needs no new handling.Sibling-site sweep
grep -rn "|| activeSessionIdRef.current" apps/desktop/srcreturns 8 non-test sites. This PR isthe complete set of the ones sharing this root cause; the other five are deliberately excluded:
use-prompt-actions/index.ts:421— the sharedhandoffSessionhelper. Its only in-repo caller isslash.ts:585, which passes an explicit id once this lands, so the fallback becomes a defensivedefault rather than a live path.
workspace-session-target.ts:44,use-cwd-actions.ts:28,use-cwd-actions.ts:72— presencetests (
if (… || activeSessionIdRef.current)), not target resolution.use-preview-routing.ts:41— already a ladder with the route ahead of the ref; different concern.The other
activeSessionIdRef.currentreads inindex.ts(cancelRun,redirectPrompt,reloadFromMessage,restoreToMessage,editMessage) are771f1b7f2's deliberate design: theyact on the foreground thread and must not resume a different conversation.
Related Issue
No filed issue — found by auditing the call sites
584b27449did not convert.Type of Change
Changes Made
apps/desktop/src/app/session/hooks/use-prompt-actions/slash.tsresolveExistingSessionIdbesideensureSessionId:resolveTargetSessionIdwith thecreate rung disabled.
/yolo(:547),/handoff(:577),/skin(:629) now resolve through it./yolosnapshots!$yoloActive.get()before the resolve so the toggle still inverts what theuser saw;
/skinapplies the theme before the resolve so it stays instant.apps/desktop/src/app/session/hooks/use-prompt-actions/slash-target-session.test.tsx— newregression file (kept out of
index.test.tsx, which several open PRs are editing).How to Test
cd apps/desktop && npx vitest run --project ui src/app/session/hooks/use-prompt-actions/→ 136 passing (5 files).
Regression guard — with
slash.tsreverted tomainand the new test file kept:/yoloonmainproducesconfig.set { session_id: 'rt-chat-a' }and issues nosession.resume;/handoffproduceshandoff.request { session_id: 'rt-chat-a' }. With the fixapplied, all 4 pass. The 4th case ("still arms
/yololocally on a new-chat draft instead ofminting a session") is green both before and after by design — it pins the behaviour the
createSession: async () => nullrung preserves.Manually: open chat A and let it stream, click chat B in the sidebar, type
/yolo. Before, thezap and the
config.setland on A. After, they land on B.Checklist
Code
fix(scope):,feat(scope):, etc.)use-prompt-actions/slash.tsnpx vitest run --project ui src/app/session/instead: 342 passing (29 files). Alsonpx eslint(clean) andnpx tsc -p . --noEmit(clean).Documentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A