Skip to content

Commit 4707e94

Browse files
committed
feat: add paused queue UX with bottom border display
- Queue pauses on Ctrl-C/Escape during response (only if non-empty) - Paused queue shows in styled bottom border with message preview - Queue auto-resumes after next successful message send - Pause state prevents unwanted auto-sends after interrupt
1 parent 3516f24 commit 4707e94

File tree

5 files changed

+101
-7
lines changed

5 files changed

+101
-7
lines changed

cli/src/chat.tsx

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
getStatusIndicatorState,
1717
} from './components/status-indicator'
1818
import { SuggestionMenu } from './components/suggestion-menu'
19-
import { Button } from './components/button'
2019
import { SLASH_COMMANDS } from './data/slash-commands'
2120
import { useAgentValidation } from './hooks/use-agent-validation'
2221
import { useAuthState } from './hooks/use-auth-state'
@@ -44,6 +43,7 @@ import { buildMessageTree } from './utils/message-tree-utils'
4443
import { computeInputLayoutMetrics } from './utils/text-layout'
4544
import { createMarkdownPalette } from './utils/theme-system'
4645
import { BORDER_CHARS } from './utils/ui-constants'
46+
import { Button } from './components/button'
4747

4848
import type { ContentBlock } from './types/chat'
4949
import type { SendMessageFn } from './types/contracts/send-message'
@@ -365,12 +365,17 @@ export const Chat = ({
365365
const {
366366
queuedMessages,
367367
streamStatus,
368+
queuePaused,
368369
streamMessageIdRef,
369370
addToQueue,
370371
startStreaming,
371372
stopStreaming,
372373
setStreamStatus,
373374
setCanProcessQueue,
375+
pauseQueue,
376+
resumeQueue,
377+
clearQueue,
378+
isQueuePausedRef,
374379
} = useMessageQueue(
375380
(content: string) =>
376381
sendMessageRef.current?.({ content, agentMode }) ?? Promise.resolve(),
@@ -414,6 +419,8 @@ export const Chat = ({
414419
lastMessageMode,
415420
setLastMessageMode,
416421
addSessionCredits,
422+
isQueuePausedRef,
423+
resumeQueue,
417424
})
418425

419426
sendMessageRef.current = sendMessage
@@ -441,6 +448,7 @@ export const Chat = ({
441448
streamMessageIdRef,
442449
addToQueue,
443450
clearMessages,
451+
clearQueue,
444452
handleCtrlC,
445453
saveToHistory,
446454
scrollToLatest,
@@ -464,6 +472,9 @@ export const Chat = ({
464472
isChainInProgressRef,
465473
scrollToLatest,
466474
handleCtrlC,
475+
clearQueue,
476+
queuedMessages,
477+
pauseQueue,
467478
],
468479
)
469480

@@ -496,6 +507,11 @@ export const Chat = ({
496507
navigateDown,
497508
toggleAgentMode,
498509
onCtrlC: handleCtrlC,
510+
onInterrupt: () => {
511+
if (queuedMessages.length > 0) {
512+
pauseQueue()
513+
}
514+
},
499515
historyNavUpEnabled,
500516
historyNavDownEnabled,
501517
})
@@ -524,12 +540,22 @@ export const Chat = ({
524540
</text>
525541
) : null
526542

527-
const shouldShowQueuePreview = queuedMessages.length > 0
543+
const shouldShowQueuePreview = queuedMessages.length > 0 && !queuePaused
528544
const queuePreviewTitle = useMemo(() => {
529545
if (!shouldShowQueuePreview) return undefined
530546
const previewWidth = Math.max(30, separatorWidth - 20)
531547
return formatQueuedPreview(queuedMessages, previewWidth)
532548
}, [queuedMessages, separatorWidth, shouldShowQueuePreview])
549+
550+
const pausedQueueText = useMemo(() => {
551+
if (!queuePaused || queuedMessages.length === 0) return undefined
552+
const count = queuedMessages.length
553+
return `${count} queued — your next message sends first`
554+
}, [queuePaused, queuedMessages])
555+
556+
const handleClearQueue = useCallback(() => {
557+
clearQueue()
558+
}, [clearQueue])
533559
const hasSlashSuggestions =
534560
slashContext.active && slashSuggestionItems.length > 0
535561
const hasMentionSuggestions =
@@ -836,6 +862,32 @@ export const Chat = ({
836862
)}
837863
</box>
838864
</box>
865+
866+
{/* Paused queue indicator - fake bottom border continuation */}
867+
{pausedQueueText && (
868+
<box style={{ width: '100%' }}>
869+
<box style={{ flexDirection: 'row', alignItems: 'center' }}>
870+
<text style={{ wrapMode: 'none', flexGrow: 1 }}>
871+
<span fg={theme.warning}>
872+
{BORDER_CHARS.vertical}{pausedQueueText}
873+
</span>
874+
</text>
875+
<Button onClick={handleClearQueue} style={{ paddingRight: 1 }}>
876+
<text>
877+
<span fg={theme.error}></span>
878+
</text>
879+
</Button>
880+
<text style={{ wrapMode: 'none' }}>
881+
<span fg={theme.warning}>{BORDER_CHARS.vertical}</span>
882+
</text>
883+
</box>
884+
<text style={{ wrapMode: 'none' }}>
885+
<span fg={theme.warning}>
886+
{BORDER_CHARS.bottomLeft}{BORDER_CHARS.horizontal.repeat(separatorWidth - 2)}{BORDER_CHARS.bottomRight}
887+
</span>
888+
</text>
889+
</box>
890+
)}
839891
</box>
840892

841893
{/* Login Modal Overlay - show when not authenticated and done checking */}

cli/src/commands/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export async function routeUserPrompt(params: {
2222
streamMessageIdRef: React.MutableRefObject<string | null>
2323
addToQueue: (message: string) => void
2424
clearMessages: () => void
25+
clearQueue: () => string[]
2526
handleCtrlC: () => true
2627
saveToHistory: (message: string) => void
2728
scrollToLatest: () => void
@@ -49,6 +50,7 @@ export async function routeUserPrompt(params: {
4950
streamMessageIdRef,
5051
addToQueue,
5152
clearMessages,
53+
clearQueue,
5254
handleCtrlC,
5355
saveToHistory,
5456
scrollToLatest,

cli/src/hooks/use-keyboard-handlers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface KeyboardHandlersConfig {
1616
navigateDown: () => void
1717
toggleAgentMode: () => void
1818
onCtrlC: () => boolean
19+
onInterrupt: () => void
1920
historyNavUpEnabled: boolean
2021
historyNavDownEnabled: boolean
2122
}
@@ -33,6 +34,7 @@ export const useKeyboardHandlers = ({
3334
navigateDown,
3435
toggleAgentMode,
3536
onCtrlC,
37+
onInterrupt,
3638
historyNavUpEnabled,
3739
historyNavDownEnabled,
3840
}: KeyboardHandlersConfig) => {
@@ -53,6 +55,7 @@ export const useKeyboardHandlers = ({
5355
if (abortControllerRef.current) {
5456
abortControllerRef.current.abort()
5557
}
58+
onInterrupt()
5659

5760
return
5861
}
@@ -68,7 +71,7 @@ export const useKeyboardHandlers = ({
6871
}
6972
}
7073
},
71-
[isStreaming, isWaitingForResponse, abortControllerRef, onCtrlC],
74+
[isStreaming, isWaitingForResponse, abortControllerRef, onCtrlC, onInterrupt],
7275
),
7376
)
7477

cli/src/hooks/use-message-queue.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ export const useMessageQueue = (
1010
const [queuedMessages, setQueuedMessages] = useState<string[]>([])
1111
const [streamStatus, setStreamStatus] = useState<StreamStatus>('idle')
1212
const [canProcessQueue, setCanProcessQueue] = useState<boolean>(true)
13+
const [queuePaused, setQueuePaused] = useState<boolean>(false)
1314

1415
const queuedMessagesRef = useRef<string[]>([])
1516
const streamTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
1617
const streamIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
1718
const streamMessageIdRef = useRef<string | null>(null)
19+
const isQueuePausedRef = useRef<boolean>(false)
1820

1921
useEffect(() => {
2022
queuedMessagesRef.current = queuedMessages
2123
}, [queuedMessages])
2224

25+
useEffect(() => {
26+
isQueuePausedRef.current = queuePaused
27+
}, [queuePaused])
28+
2329
const clearStreaming = useCallback(() => {
2430
if (streamTimeoutRef.current) {
2531
clearTimeout(streamTimeoutRef.current)
@@ -41,7 +47,7 @@ export const useMessageQueue = (
4147
}, [clearStreaming])
4248

4349
useEffect(() => {
44-
if (!canProcessQueue) return
50+
if (!canProcessQueue || queuePaused) return
4551
if (streamStatus !== 'idle') return
4652
if (streamMessageIdRef.current) return
4753
if (isChainInProgressRef.current) return
@@ -61,6 +67,7 @@ export const useMessageQueue = (
6167
return () => clearTimeout(timeoutId)
6268
}, [
6369
canProcessQueue,
70+
queuePaused,
6471
streamStatus,
6572
sendMessage,
6673
isChainInProgressRef,
@@ -73,26 +80,48 @@ export const useMessageQueue = (
7380
setQueuedMessages(newQueue)
7481
}, [])
7582

83+
const pauseQueue = useCallback(() => {
84+
setQueuePaused(true)
85+
setCanProcessQueue(false)
86+
}, [])
87+
88+
const resumeQueue = useCallback(() => {
89+
setQueuePaused(false)
90+
setCanProcessQueue(true)
91+
}, [])
92+
93+
const clearQueue = useCallback(() => {
94+
const current = queuedMessagesRef.current
95+
queuedMessagesRef.current = []
96+
setQueuedMessages([])
97+
return current
98+
}, [])
99+
76100
const startStreaming = useCallback(() => {
77101
setStreamStatus('streaming')
78102
setCanProcessQueue(false)
79103
}, [])
80104

81105
const stopStreaming = useCallback(() => {
82106
setStreamStatus('idle')
83-
setCanProcessQueue(true)
84-
}, [])
107+
setCanProcessQueue(!queuePaused)
108+
}, [queuePaused])
85109

86110
return {
87111
queuedMessages,
88112
streamStatus,
89113
canProcessQueue,
114+
queuePaused,
90115
streamMessageIdRef,
91116
addToQueue,
92117
startStreaming,
93118
stopStreaming,
94119
setStreamStatus,
95120
clearStreaming,
96121
setCanProcessQueue,
122+
pauseQueue,
123+
resumeQueue,
124+
clearQueue,
125+
isQueuePausedRef,
97126
}
98127
}

cli/src/hooks/use-send-message.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,8 @@ interface UseSendMessageOptions {
233233
lastMessageMode: AgentMode | null
234234
setLastMessageMode: (mode: AgentMode | null) => void
235235
addSessionCredits: (credits: number) => void
236+
isQueuePausedRef?: React.MutableRefObject<boolean>
237+
resumeQueue?: () => void
236238
}
237239

238240
export const useSendMessage = ({
@@ -264,6 +266,8 @@ export const useSendMessage = ({
264266
lastMessageMode,
265267
setLastMessageMode,
266268
addSessionCredits,
269+
isQueuePausedRef,
270+
resumeQueue,
267271
}: UseSendMessageOptions): {
268272
sendMessage: SendMessageFn
269273
clearMessages: () => void
@@ -785,7 +789,7 @@ export const useSendMessage = ({
785789
abortControllerRef.current = abortController
786790
abortController.signal.addEventListener('abort', () => {
787791
setStreamStatus('idle')
788-
setCanProcessQueue(true)
792+
setCanProcessQueue(!isQueuePausedRef?.current)
789793
updateChainInProgress(false)
790794
timerController.stop('aborted')
791795

@@ -1581,6 +1585,9 @@ export const useSendMessage = ({
15811585
}
15821586

15831587
setStreamStatus('idle')
1588+
if (resumeQueue) {
1589+
resumeQueue()
1590+
}
15841591
setCanProcessQueue(true)
15851592
updateChainInProgress(false)
15861593
const timerResult = timerController.stop('success')
@@ -1675,6 +1682,7 @@ export const useSendMessage = ({
16751682
lastMessageMode,
16761683
setLastMessageMode,
16771684
addSessionCredits,
1685+
resumeQueue,
16781686
],
16791687
)
16801688

0 commit comments

Comments
 (0)