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
17 changes: 14 additions & 3 deletions apps/desktop/src/app/chat/composer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { $statusItemsBySession } from '@/store/composer-status'
import { notify } from '@/store/notifications'
import { $previewStatusBySession } from '@/store/preview-status'
import { listRepoBranches, requestStartWorkSession, startWorkInRepo, switchBranchInRepo } from '@/store/projects'
import { $activeSessionAwaitingInput } from '@/store/prompts'
import { toggleReview } from '@/store/review'
import { $gatewayState, $messages, setSessionPickerOpen } from '@/store/session'
import { $threadScrolledUp } from '@/store/thread-scroll'
Expand Down Expand Up @@ -229,6 +230,11 @@ export function ChatBar({
const statusItemsBySession = useStore($statusItemsBySession)
const previewStatusBySession = useStore($previewStatusBySession)
const scrolledUp = useStore($threadScrolledUp)
// The turn is parked on the user (clarify / approval / sudo / secret). Esc must
// not interrupt it — there's nothing actively running to stop, and stopping
// would discard a question the user may want to come back to. The blocking
// prompt owns its own dismissal (Skip, Reject, dialog close).
const awaitingInput = useStore($activeSessionAwaitingInput)
// Pop-out is a shared, persisted state — but secondary windows (the Ctrl+Shift+N
// tiny window, subagent watch windows) always start docked and can't pop out:
// a floating composer makes no sense in a single-session side window, and it
Expand Down Expand Up @@ -1214,8 +1220,10 @@ export function ChatBar({
return
}

// Otherwise Esc interrupts the running turn (Stop-button parity).
if (busy) {
// Otherwise Esc interrupts the running turn (Stop-button parity) — unless
// the turn is parked waiting on the user, where Esc must not discard the
// pending prompt.
if (busy && !awaitingInput) {
event.preventDefault()
triggerHaptic('cancel')
void Promise.resolve(onCancel())
Expand Down Expand Up @@ -1779,7 +1787,10 @@ export function ChatBar({
const escCancelRef = useRef<(event: globalThis.KeyboardEvent) => void>(() => {})

escCancelRef.current = (event: globalThis.KeyboardEvent) => {
if (event.key !== 'Escape' || event.defaultPrevented || !busy) {
// `awaitingInput`: the turn is parked on a clarify / approval / sudo / secret
// prompt, which owns Esc (or is meant to persist) — never cancel the stream
// out from under it.
if (event.key !== 'Escape' || event.defaultPrevented || !busy || awaitingInput) {
return
}

Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/app/session/hooks/use-message-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { triggerHaptic } from '@/lib/haptics'
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
import { parseTodos } from '@/lib/todos'
import { setClarifyRequest } from '@/store/clarify'
import { clearClarifyRequest, setClarifyRequest } from '@/store/clarify'
import { setSessionCompacting } from '@/store/compaction'
import { refreshBackgroundProcesses } from '@/store/composer-status'
import { $gateway } from '@/store/gateway'
Expand Down Expand Up @@ -913,6 +913,7 @@ export function useMessageStream({
// session so a background turn finishing can't wipe the active chat's
// prompt, and vice versa.
clearAllPrompts(sessionId)
clearClarifyRequest(undefined, sessionId)
setSessionCompacting(sessionId, false)

flushQueuedDeltas(sessionId)
Expand Down Expand Up @@ -1185,6 +1186,7 @@ export function useMessageStream({
// the failed turn (same intent as the message.complete clear).
if (sessionId) {
clearAllPrompts(sessionId)
clearClarifyRequest(undefined, sessionId)
setSessionCompacting(sessionId, false)
compactedTurnRef.current.delete(sessionId)
}
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/app/session/hooks/use-prompt-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { triggerHaptic } from '@/lib/haptics'
import { setMutableRef } from '@/lib/mutable-ref'
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
import { setSessionYolo } from '@/lib/yolo-session'
import { clearClarifyRequest } from '@/store/clarify'
import { openCommandPalettePage } from '@/store/command-palette'
import {
$composerAttachments,
Expand All @@ -44,6 +45,7 @@ import { setPetScale } from '@/store/pet-gallery'
import { $petGenInput, openPetGenerate } from '@/store/pet-generate'
import { clearPreviewArtifacts } from '@/store/preview-status'
import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile, normalizeProfileKey } from '@/store/profile'
import { clearAllPrompts } from '@/store/prompts'
import {
$busy,
$connection,
Expand Down Expand Up @@ -1535,13 +1537,20 @@ export function usePromptActions({
awaitingResponse: false,
streamId: null,
pendingBranchGroup: null,
needsInput: false,
interrupted: true
}
})

clearSessionTodos(sessionId)
clearSessionSubagents(sessionId)
resetSessionBackground(sessionId)
// Stop ends the turn, so the gateway is no longer blocked on any prompt it
// raised. Drop this session's pending clarify / approval / sudo / secret so
// a dead panel (and the sidebar "needs input" dot) can't linger and accept
// an answer the backend will reject.
clearAllPrompts(sessionId)
clearClarifyRequest(undefined, sessionId)

try {
await requestGateway('session.interrupt', { session_id: sessionId })
Expand Down
Loading
Loading