Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
}

if (busy) {
// Don't re-arm busy from a stale session.info if the user
// just clicked Stop (interrupted=true). The backend's
// cooperative interrupt may not have propagated yet, so
// running is still true in the heartbeat. The turn's
// finally block will emit running=false to clear busy.
if (state.interrupted) {
return state
}

return {
...state,
busy,
Expand Down Expand Up @@ -307,14 +316,27 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
triggerHaptic('streamStart')
}

updateSessionState(sessionId, state => ({
...state,
busy: true,
awaitingResponse: true,
sawAssistantPayload: false,
interrupted: false,
turnStartedAt: Date.now()
}))
updateSessionState(sessionId, state => {
// If the user clicked Stop (cancelRun set interrupted=true), don't
// let a stale message.start from a chained turn (goal follow-up,
// completion drain) or an in-flight LLM response re-arm busy.
// The interrupt is user intent — the backend's cooperative cancel
// may not have propagated yet, so its events are stale. The turn's
// finally block will emit session.info with running=false to clear
// busy for real once the agent loop actually exits.
if (state.interrupted) {
return state
}

return {
...state,
busy: true,
awaitingResponse: true,
sawAssistantPayload: false,
interrupted: false,
turnStartedAt: Date.now()
}
})

if (isActiveEvent) {
setTurnStartedAt(Date.now())
Expand Down
12 changes: 10 additions & 2 deletions apps/desktop/src/app/session/hooks/use-prompt-actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,16 @@ export function usePromptActions({
)

const cancelRun = useCallback(async () => {
const sessionId = activeSessionId || activeSessionIdRef.current
// Read from the ref, not the closure-captured `activeSessionId`. The
// actions bag is a stable ref mutated in place (Object.assign on each
// ContribWiring render), and ChatRoutesSurface is memoized on that stable
// ref — so it does NOT re-render when activeSessionId changes, which means
// the ChatView element's onCancel prop holds a stale cancelRun closure.
// The closure's `activeSessionId` can be a previous session's id (or null
// from a new-chat draft), sending session.interrupt to the wrong session.
// The ref is updated via useEffect on every activeSessionId change, so it
// always reflects the current session — same pattern submitText uses.
const sessionId = activeSessionIdRef.current

const releaseBusy = () => {
setMutableRef(busyRef, false)
Expand Down Expand Up @@ -589,7 +598,6 @@ export function usePromptActions({
notifyError(stopError, copy.stopFailed)
}
}, [
activeSessionId,
activeSessionIdRef,
busyRef,
copy.stopFailed,
Expand Down
Loading