Skip to content

Commit fd1f8d0

Browse files
committed
feat(cli): prevent auto-scroll during user-initiated collapses and add
scroll indicator - Add isUserCollapsingRef to track user-initiated collapse actions - Prevent auto-scroll when user manually collapses agent sections - Add clickable scroll indicator (↓) when not at bottom of chat - Refactor diff-viewer to use single text element for better rendering - Add object validation in agent-branch-item and tool-call-item - Restructure status bar with three-section flexbox layout
1 parent a988da8 commit fd1f8d0

File tree

8 files changed

+646
-41
lines changed

8 files changed

+646
-41
lines changed

cli/src/chat.tsx

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type { SendMessageTimerEvent } from './hooks/use-send-message'
4040
import type { ContentBlock } from './types/chat'
4141
import type { SendMessageFn } from './types/contracts/send-message'
4242
import type { KeyEvent, ScrollBoxRenderable } from '@opentui/core'
43+
import { TextAttributes } from '@opentui/core'
4344

4445
const MAX_VIRTUALIZED_TOP_LEVEL = 60
4546
const VIRTUAL_OVERSCAN = 12
@@ -729,7 +730,8 @@ export const Chat = ({
729730
) : null
730731

731732
const shouldShowQueuePreview = queuedMessages.length > 0
732-
const shouldShowStatusLine = Boolean(hasStatus || shouldShowQueuePreview)
733+
const shouldShowStatusLine =
734+
hasStatus || shouldShowQueuePreview || !isAtBottom
733735

734736
const statusIndicatorNode = (
735737
<StatusIndicator
@@ -836,18 +838,45 @@ export const Chat = ({
836838
width: '100%',
837839
}}
838840
>
839-
<text style={{ wrapMode: 'none' }}>
840-
{hasStatus && statusIndicatorNode}
841+
{/* Left section - queue preview */}
842+
<box style={{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }}>
841843
{shouldShowQueuePreview && (
842-
<span fg={theme.secondary} bg={theme.inputFocusedBg}>
843-
{' '}
844-
{formatQueuedPreview(
845-
queuedMessages,
846-
Math.max(30, terminalWidth - 25),
847-
)}{' '}
848-
</span>
844+
<text style={{ wrapMode: 'none' }}>
845+
<span fg={theme.secondary} bg={theme.inputFocusedBg}>
846+
{` ${formatQueuedPreview(
847+
queuedMessages,
848+
Math.max(30, terminalWidth - 25),
849+
)} `}
850+
</span>
851+
</text>
849852
)}
850-
</text>
853+
</box>
854+
855+
{/* Center section - scroll indicator (always centered) */}
856+
<box style={{ flexShrink: 0 }}>
857+
{!isAtBottom && (
858+
<text onMouseDown={scrollToLatest}>
859+
<span fg={theme.info} attributes={TextAttributes.BOLD}>
860+
861+
</span>
862+
</text>
863+
)}
864+
</box>
865+
866+
{/* Right section - status indicator */}
867+
<box
868+
style={{
869+
flexGrow: 1,
870+
flexShrink: 1,
871+
flexBasis: 0,
872+
flexDirection: 'row',
873+
justifyContent: 'flex-end',
874+
}}
875+
>
876+
{hasStatus && (
877+
<text style={{ wrapMode: 'none' }}>{statusIndicatorNode}</text>
878+
)}
879+
</box>
851880
</box>
852881
)}
853882
<box

cli/src/components/agent-branch-item.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface AgentBranchItemProps {
1818
statusIndicator?: string
1919
onToggle?: () => void
2020
titleSuffix?: string
21+
isUserCollapsingRef?: React.MutableRefObject<boolean>
2122
}
2223

2324
export const AgentBranchItem = ({
@@ -34,6 +35,7 @@ export const AgentBranchItem = ({
3435
statusIndicator = '●',
3536
onToggle,
3637
titleSuffix,
38+
isUserCollapsingRef,
3739
}: AgentBranchItemProps) => {
3840
const theme = useTheme()
3941

@@ -142,6 +144,12 @@ export const AgentBranchItem = ({
142144
)
143145
}
144146

147+
// Check if value is a plain object (not a React element)
148+
if (typeof value === 'object' && value !== null && !React.isValidElement(value)) {
149+
console.warn('Attempted to render plain object in agent content:', value)
150+
return null
151+
}
152+
145153
return (
146154
<box key="expanded-unknown" style={{ flexDirection: 'column', gap: 0 }}>
147155
{value}
@@ -281,7 +289,13 @@ export const AgentBranchItem = ({
281289
alignSelf: 'flex-end',
282290
marginTop: 1,
283291
}}
284-
onMouseDown={onToggle}
292+
onMouseDown={() => {
293+
// Set flag to prevent auto-scroll during user-initiated collapse
294+
if (isUserCollapsingRef) {
295+
isUserCollapsingRef.current = true
296+
}
297+
onToggle()
298+
}}
285299
>
286300
<text
287301
fg={theme.secondary}

cli/src/components/message-block.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ interface MessageBlockProps {
4444
collapsedAgents: Set<string>
4545
streamingAgents: Set<string>
4646
onToggleCollapsed: (id: string) => void
47-
isCollapsingRef?: React.MutableRefObject<boolean>
47+
isUserCollapsingRef?: React.MutableRefObject<boolean>
4848
}
4949

5050
export const MessageBlock = ({
@@ -67,7 +67,7 @@ export const MessageBlock = ({
6767
collapsedAgents,
6868
streamingAgents,
6969
onToggleCollapsed,
70-
isCollapsingRef,
70+
isUserCollapsingRef,
7171
}: MessageBlockProps): ReactNode => {
7272
const theme = useTheme()
7373
const resolvedTextColor = textColor ?? theme.foreground
@@ -299,7 +299,7 @@ export const MessageBlock = ({
299299
statusColor={statusColor}
300300
statusIndicator={statusIndicator}
301301
onToggle={() => onToggleCollapsed(agentBlock.agentId)}
302-
isCollapsingRef={isCollapsingRef}
302+
isUserCollapsingRef={isUserCollapsingRef}
303303
/>
304304
</box>
305305
)

cli/src/components/tools/diff-viewer.tsx

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,25 +41,21 @@ const lineColor = (line: string): { fg: string; attrs?: number } => {
4141
export const DiffViewer = ({ diffText }: DiffViewerProps) => {
4242
const theme = useTheme()
4343
const lines = diffText.split('\n')
44+
const filteredLines = lines.filter((rawLine) => !rawLine.startsWith('@@'))
4445

4546
return (
46-
<box
47-
style={{ flexDirection: 'column', gap: 0, width: '100%', flexGrow: 1 }}
48-
>
49-
{lines
50-
.filter((rawLine) => !rawLine.startsWith('@@'))
51-
.map((rawLine, idx) => {
52-
const line = rawLine.length === 0 ? ' ' : rawLine
53-
const { fg, attrs } = lineColor(line)
54-
const resolvedFg = fg || theme.foreground
55-
return (
56-
<text key={`diff-line-${idx}`} style={{ wrapMode: 'none' }}>
57-
<span fg={resolvedFg} attributes={attrs}>
58-
{line}
59-
</span>
60-
</text>
61-
)
62-
})}
63-
</box>
47+
<text style={{ wrapMode: 'none', marginTop: 0, marginBottom: 0 }}>
48+
{filteredLines.map((rawLine, idx) => {
49+
const line = rawLine.length === 0 ? ' ' : rawLine
50+
const { fg, attrs } = lineColor(line)
51+
const resolvedFg = fg || theme.foreground
52+
return (
53+
<span key={`diff-line-${idx}`} fg={resolvedFg} attributes={attrs}>
54+
{line}
55+
{idx < filteredLines.length - 1 ? '\n' : ''}
56+
</span>
57+
)
58+
})}
59+
</text>
6460
)
6561
}

cli/src/components/tools/str-replace.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,16 @@ const EditHeader = ({ name, filePath }: EditHeaderProps) => {
5050
const bulletChar = '• '
5151

5252
return (
53-
<box style={{ flexDirection: 'row', alignItems: 'center', width: '100%' }}>
54-
<text style={{ wrapMode: 'word' }}>
53+
<box
54+
style={{
55+
flexDirection: 'row',
56+
alignItems: 'center',
57+
width: '100%',
58+
marginTop: 0,
59+
marginBottom: 0,
60+
}}
61+
>
62+
<text style={{ wrapMode: 'word', marginTop: 0, marginBottom: 0 }}>
5563
<span fg={theme.foreground}>{bulletChar}</span>
5664
<span fg={theme.foreground} attributes={TextAttributes.BOLD}>
5765
{name}
@@ -69,12 +77,34 @@ interface EditBodyProps {
6977
}
7078

7179
const EditBody = ({ name, filePath, diffText }: EditBodyProps) => {
80+
const hasDiff = diffText && diffText.trim().length > 0
81+
7282
return (
73-
<box style={{ flexDirection: 'column', gap: 0, width: '100%' }}>
83+
<box
84+
style={{
85+
flexDirection: 'column',
86+
gap: 0,
87+
width: '100%',
88+
marginTop: 0,
89+
marginBottom: 0,
90+
}}
91+
>
7492
<EditHeader name={name} filePath={filePath} />
75-
<box style={{ paddingLeft: 2, width: '100%' }}>
76-
<DiffViewer diffText={diffText} />
77-
</box>
93+
{hasDiff && (
94+
<box
95+
style={{
96+
paddingLeft: 2,
97+
paddingRight: 0,
98+
paddingTop: 0,
99+
paddingBottom: 0,
100+
width: '100%',
101+
marginTop: 0,
102+
marginBottom: 0,
103+
}}
104+
>
105+
<DiffViewer diffText={diffText} />
106+
</box>
107+
)}
78108
</box>
79109
)
80110
}

cli/src/components/tools/tool-call-item.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ const renderExpandedContent = (
110110
)
111111
}
112112

113+
// Check if value is a plain object (not a React element)
114+
if (typeof value === 'object' && value !== null && !React.isValidElement(value)) {
115+
console.warn('Attempted to render plain object in tool content:', value)
116+
return null
117+
}
118+
113119
return (
114120
<box
115121
key="tool-expanded-unknown"

0 commit comments

Comments
 (0)