Skip to content

Commit ccf2b79

Browse files
committed
feat(cli): add Ctrl+T shortcut to toggle collapse/expand all
- Add toggle-all (Ctrl+T) keyboard shortcut that collapses all if any are expanded, otherwise expands all - Add setAllBlocksCollapsedState and hasAnyExpandedBlocks helpers for toggling collapsible blocks - Update help banner with new shortcut and improved layout using columnGap - Add comprehensive unit tests for keyboard actions and collapse helpers
1 parent 6b7e0f7 commit ccf2b79

File tree

9 files changed

+1457
-9
lines changed

9 files changed

+1457
-9
lines changed

cli/src/chat.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export const Chat = ({
176176
handleCollapseToggle,
177177
isUserCollapsing,
178178
handleLoadPreviousMessages,
179+
handleToggleAll,
179180
} = useChatMessages({ messages, setMessages })
180181

181182
// Use extracted UI hook for scroll, terminal dimensions, and theme
@@ -1040,6 +1041,7 @@ export const Chat = ({
10401041
},
10411042
onScrollUp: scrollUp,
10421043
onScrollDown: scrollDown,
1044+
onToggleAll: handleToggleAll,
10431045
onOpenBuyCredits: () => {
10441046
// If credits have been restored, just return to default mode
10451047
if (areCreditsRestored()) {
@@ -1081,6 +1083,7 @@ export const Chat = ({
10811083
clearQueue,
10821084
scrollUp,
10831085
scrollDown,
1086+
handleToggleAll,
10841087
],
10851088
)
10861089

cli/src/components/chat-input-bar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const ChatInputBar = ({
121121
const handleKeyIntercept = useEvent(
122122
(key: {
123123
name?: string
124+
sequence?: string
124125
shift?: boolean
125126
ctrl?: boolean
126127
meta?: boolean

cli/src/components/help-banner.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,18 @@ export const HelpBanner = () => {
5151
{/* Shortcuts Section */}
5252
<box style={{ flexDirection: 'column', gap: 0 }}>
5353
<SectionHeader>Shortcuts</SectionHeader>
54-
<box style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 2, paddingLeft: 2 }}>
54+
<box style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 2, paddingLeft: 2 }}>
5555
<Shortcut keys="Ctrl+C / Esc" action="stop" />
5656
<Shortcut keys="Ctrl+J / Opt+Enter" action="newline" />
5757
<Shortcut keys="↑↓" action="history" />
58+
<Shortcut keys="Ctrl+T" action="collapse/expand agents" />
5859
</box>
5960
</box>
6061

6162
{/* Features Section */}
6263
<box style={{ flexDirection: 'column', gap: 0 }}>
6364
<SectionHeader>Features</SectionHeader>
64-
<box style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 2, paddingLeft: 2 }}>
65+
<box style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 2, paddingLeft: 2 }}>
6566
<Shortcut keys="/" action="commands" />
6667
<Shortcut keys="@files" action="mention" />
6768
<Shortcut keys="@agents" action="use agent" />
@@ -73,7 +74,7 @@ export const HelpBanner = () => {
7374
<box style={{ flexDirection: 'column', gap: 0 }}>
7475
<SectionHeader>Credits</SectionHeader>
7576
<box style={{ flexDirection: 'column', paddingLeft: 2 }}>
76-
<box style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 1 }}>
77+
<box style={{ flexDirection: 'row', flexWrap: 'wrap', columnGap: 1 }}>
7778
<text style={{ fg: theme.foreground }}>1 credit = 1 cent</text>
7879
<text style={{ fg: theme.muted }}>·</text>
7980
<text style={{ fg: theme.foreground }}>/buy-credits</text>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export type ChatKeyboardHandlers = {
7979
onScrollUp: () => void
8080
onScrollDown: () => void
8181

82+
// Toggle all handler
83+
onToggleAll: () => void
84+
8285
// Out of credits handler
8386
onOpenBuyCredits: () => void
8487
}
@@ -239,6 +242,9 @@ function dispatchAction(
239242
case 'scroll-down':
240243
handlers.onScrollDown()
241244
return true
245+
case 'toggle-all':
246+
handlers.onToggleAll()
247+
return true
242248
case 'open-buy-credits':
243249
handlers.onOpenBuyCredits()
244250
return true

cli/src/hooks/use-chat-messages.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
77

88
import { buildMessageTree } from '../utils/message-tree-utils'
9+
import { setAllBlocksCollapsedState, hasAnyExpandedBlocks } from '../utils/collapse-helpers'
910

1011
import type { ChatMessage, ContentBlock } from '../types/chat'
1112

@@ -42,6 +43,8 @@ export interface UseChatMessagesReturn {
4243
isUserCollapsing: () => boolean
4344
/** Handler to load more previous messages */
4445
handleLoadPreviousMessages: () => void
46+
/** Handler to toggle all collapsed/expanded state in all AI responses */
47+
handleToggleAll: () => void
4548
}
4649

4750
/**
@@ -181,7 +184,9 @@ export function useChatMessages({
181184
})
182185
})
183186

184-
// Reset flag after state update completes
187+
// Reset flag after state update completes.
188+
// Uses setTimeout(0) to defer until after React's batched state updates
189+
// have been applied, ensuring the flag stays true during the render cycle.
185190
setTimeout(() => {
186191
isUserCollapsingRef.current = false
187192
}, 0)
@@ -196,6 +201,27 @@ export function useChatMessages({
196201
setVisibleMessageCount((prev) => prev + MESSAGE_BATCH_SIZE)
197202
}, [])
198203

204+
/**
205+
* Toggles all collapsible blocks in all AI responses.
206+
* If any block is expanded, collapses all. Otherwise expands all.
207+
*/
208+
const handleToggleAll = useCallback(() => {
209+
isUserCollapsingRef.current = true
210+
211+
setMessages((prevMessages) => {
212+
// Determine target state: if any expanded, collapse all; otherwise expand all
213+
const shouldCollapse = hasAnyExpandedBlocks(prevMessages)
214+
return setAllBlocksCollapsedState(prevMessages, shouldCollapse)
215+
})
216+
217+
// Reset flag after state update completes.
218+
// Uses setTimeout(0) to defer until after React's batched state updates
219+
// have been applied, ensuring the flag stays true during the render cycle.
220+
setTimeout(() => {
221+
isUserCollapsingRef.current = false
222+
}, 0)
223+
}, [setMessages])
224+
199225
// Build message tree from flat messages array
200226
const { tree: messageTree, topLevelMessages } = useMemo(
201227
() => buildMessageTree(messages),
@@ -221,5 +247,6 @@ export function useChatMessages({
221247
handleCollapseToggle,
222248
isUserCollapsing,
223249
handleLoadPreviousMessages,
250+
handleToggleAll,
224251
}
225252
}

0 commit comments

Comments
 (0)