Skip to content

Commit c4239ec

Browse files
committed
refactor(cli): extract ChatInputBar component from Chat
- Create new ChatInputBar component to encapsulate input UI logic - Move suggestion menus, input field, and mode toggle to new component - Remove ~100 lines from chat.tsx for better maintainability - Clean up unused imports in Chat component
1 parent 9de7743 commit c4239ec

File tree

2 files changed

+211
-103
lines changed

2 files changed

+211
-103
lines changed

cli/src/chat.tsx

Lines changed: 31 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,12 @@ import { useKeyboard } from '@opentui/react'
33
import { useShallow } from 'zustand/react/shallow'
44

55
import { routeUserPrompt } from './commands/router'
6-
import { AgentModeToggle } from './components/agent-mode-toggle'
76
import { MessageWithAgents } from './components/message-with-agents'
8-
import { FeedbackContainer } from './components/feedback-container'
97
import { useFeedbackStore } from './state/feedback-store'
10-
import {
11-
MultilineInput,
12-
type MultilineInputHandle,
13-
} from './components/multiline-input'
14-
import { UsageBanner } from './components/usage-banner'
8+
import type { MultilineInputHandle } from './components/multiline-input'
9+
import { ChatInputBar } from './components/chat-input-bar'
1510
import { getStatusIndicatorState } from './utils/status-indicator-state'
1611
import { StatusBar } from './components/status-bar'
17-
import { SuggestionMenu } from './components/suggestion-menu'
1812
import { SLASH_COMMANDS } from './data/slash-commands'
1913
import { useAgentValidation } from './hooks/use-agent-validation'
2014
import { useChatInput } from './hooks/use-chat-input'
@@ -930,101 +924,35 @@ export const Chat = ({
930924
/>
931925
)}
932926

933-
{/* Wrap the input row in a single OpenTUI border so the toggle stays inside the flex layout.
934-
Non-actionable queue context is injected via the border title to keep the content
935-
area stable while still surfacing that information. */}
936-
{feedbackMode ? (
937-
<FeedbackContainer
938-
inputRef={inputRef}
939-
onExitFeedback={handleExitFeedback}
940-
width={separatorWidth}
941-
/>
942-
) : (
943-
<>
944-
<box
945-
title={inputBoxTitle}
946-
titleAlignment="center"
947-
style={{
948-
width: '100%',
949-
borderStyle: 'single',
950-
borderColor: theme.foreground,
951-
customBorderChars: BORDER_CHARS,
952-
paddingLeft: 1,
953-
paddingRight: 1,
954-
paddingTop: 0,
955-
paddingBottom: 0,
956-
flexDirection: 'column',
957-
gap: hasSuggestionMenu ? 1 : 0,
958-
}}
959-
>
960-
{hasSlashSuggestions ? (
961-
<SuggestionMenu
962-
items={slashSuggestionItems}
963-
selectedIndex={slashSelectedIndex}
964-
maxVisible={10}
965-
prefix="/"
966-
/>
967-
) : null}
968-
{hasMentionSuggestions ? (
969-
<SuggestionMenu
970-
items={[...agentSuggestionItems, ...fileSuggestionItems]}
971-
selectedIndex={agentSelectedIndex}
972-
maxVisible={10}
973-
prefix="@"
974-
/>
975-
) : null}
976-
<box
977-
style={{
978-
flexDirection: 'column',
979-
justifyContent: shouldCenterInputVertically
980-
? 'center'
981-
: 'flex-start',
982-
minHeight: shouldCenterInputVertically ? 3 : undefined,
983-
gap: 0,
984-
}}
985-
>
986-
<box
987-
style={{
988-
flexDirection: 'row',
989-
alignItems: shouldCenterInputVertically
990-
? 'center'
991-
: 'flex-start',
992-
width: '100%',
993-
}}
994-
>
995-
<box style={{ flexGrow: 1, minWidth: 0 }}>
996-
<MultilineInput
997-
value={inputValue}
998-
onChange={setInputValue}
999-
onSubmit={handleSubmit}
1000-
placeholder={inputPlaceholder}
1001-
focused={inputFocused && !feedbackMode}
1002-
maxHeight={Math.floor(terminalHeight / 2)}
1003-
width={inputWidth}
1004-
onKeyIntercept={handleSuggestionMenuKey}
1005-
textAttributes={theme.messageTextAttributes}
1006-
ref={inputRef}
1007-
cursorPosition={cursorPosition}
1008-
/>
1009-
</box>
1010-
<box
1011-
style={{
1012-
flexShrink: 0,
1013-
paddingLeft: 2,
1014-
}}
1015-
>
1016-
<AgentModeToggle
1017-
mode={agentMode}
1018-
onToggle={toggleAgentMode}
1019-
onSelectMode={setAgentMode}
1020-
/>
1021-
</box>
1022-
</box>
1023-
</box>
1024-
</box>
1025-
<UsageBanner />
1026-
</>
1027-
)}
927+
<ChatInputBar
928+
inputValue={inputValue}
929+
cursorPosition={cursorPosition}
930+
setInputValue={setInputValue}
931+
inputFocused={inputFocused}
932+
inputRef={inputRef}
933+
inputPlaceholder={inputPlaceholder}
934+
inputWidth={inputWidth}
935+
agentMode={agentMode}
936+
toggleAgentMode={toggleAgentMode}
937+
setAgentMode={setAgentMode}
938+
hasSlashSuggestions={hasSlashSuggestions}
939+
hasMentionSuggestions={hasMentionSuggestions}
940+
hasSuggestionMenu={hasSuggestionMenu}
941+
slashSuggestionItems={slashSuggestionItems}
942+
agentSuggestionItems={agentSuggestionItems}
943+
fileSuggestionItems={fileSuggestionItems}
944+
slashSelectedIndex={slashSelectedIndex}
945+
agentSelectedIndex={agentSelectedIndex}
946+
handleSuggestionMenuKey={handleSuggestionMenuKey}
947+
theme={theme}
948+
terminalHeight={terminalHeight}
949+
separatorWidth={separatorWidth}
950+
shouldCenterInputVertically={shouldCenterInputVertically}
951+
inputBoxTitle={inputBoxTitle}
952+
feedbackMode={feedbackMode}
953+
handleExitFeedback={handleExitFeedback}
954+
handleSubmit={handleSubmit}
955+
/>
1028956

1029957
</box>
1030958

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React from 'react'
2+
import { AgentModeToggle } from './agent-mode-toggle'
3+
import { FeedbackContainer } from './feedback-container'
4+
import { MultilineInput, type MultilineInputHandle } from './multiline-input'
5+
import { SuggestionMenu, type SuggestionItem } from './suggestion-menu'
6+
import { UsageBanner } from './usage-banner'
7+
import { BORDER_CHARS } from '../utils/ui-constants'
8+
import { useTheme } from '../hooks/use-theme'
9+
import type { AgentMode } from '../utils/constants'
10+
import type { InputValue } from '../state/chat-store'
11+
12+
type Theme = ReturnType<typeof useTheme>
13+
14+
interface ChatInputBarProps {
15+
// Input state
16+
inputValue: string
17+
cursorPosition: number
18+
setInputValue: (value: InputValue | ((prev: InputValue) => InputValue)) => void
19+
inputFocused: boolean
20+
inputRef: React.MutableRefObject<MultilineInputHandle | null>
21+
inputPlaceholder: string
22+
inputWidth: number
23+
24+
// Agent mode
25+
agentMode: AgentMode
26+
toggleAgentMode: () => void
27+
setAgentMode: (mode: AgentMode) => void
28+
29+
// Suggestion menus
30+
hasSlashSuggestions: boolean
31+
hasMentionSuggestions: boolean
32+
hasSuggestionMenu: boolean
33+
slashSuggestionItems: SuggestionItem[]
34+
agentSuggestionItems: SuggestionItem[]
35+
fileSuggestionItems: SuggestionItem[]
36+
slashSelectedIndex: number
37+
agentSelectedIndex: number
38+
handleSuggestionMenuKey: (key: any) => boolean
39+
40+
// Layout
41+
theme: Theme
42+
terminalHeight: number
43+
separatorWidth: number
44+
shouldCenterInputVertically: boolean
45+
inputBoxTitle: string | undefined
46+
47+
// Feedback mode
48+
feedbackMode: boolean
49+
handleExitFeedback: () => void
50+
51+
// Handlers
52+
handleSubmit: () => Promise<void>
53+
}
54+
55+
export const ChatInputBar = ({
56+
inputValue,
57+
cursorPosition,
58+
setInputValue,
59+
inputFocused,
60+
inputRef,
61+
inputPlaceholder,
62+
inputWidth,
63+
agentMode,
64+
toggleAgentMode,
65+
setAgentMode,
66+
hasSlashSuggestions,
67+
hasMentionSuggestions,
68+
hasSuggestionMenu,
69+
slashSuggestionItems,
70+
agentSuggestionItems,
71+
fileSuggestionItems,
72+
slashSelectedIndex,
73+
agentSelectedIndex,
74+
handleSuggestionMenuKey,
75+
theme,
76+
terminalHeight,
77+
separatorWidth,
78+
shouldCenterInputVertically,
79+
inputBoxTitle,
80+
feedbackMode,
81+
handleExitFeedback,
82+
handleSubmit,
83+
}: ChatInputBarProps) => {
84+
if (feedbackMode) {
85+
return (
86+
<FeedbackContainer
87+
inputRef={inputRef}
88+
onExitFeedback={handleExitFeedback}
89+
width={separatorWidth}
90+
/>
91+
)
92+
}
93+
94+
return (
95+
<>
96+
<box
97+
title={inputBoxTitle}
98+
titleAlignment="center"
99+
style={{
100+
width: '100%',
101+
borderStyle: 'single',
102+
borderColor: theme.foreground,
103+
customBorderChars: BORDER_CHARS,
104+
paddingLeft: 1,
105+
paddingRight: 1,
106+
paddingTop: 0,
107+
paddingBottom: 0,
108+
flexDirection: 'column',
109+
gap: hasSuggestionMenu ? 1 : 0,
110+
}}
111+
>
112+
{hasSlashSuggestions ? (
113+
<SuggestionMenu
114+
items={slashSuggestionItems}
115+
selectedIndex={slashSelectedIndex}
116+
maxVisible={10}
117+
prefix="/"
118+
/>
119+
) : null}
120+
{hasMentionSuggestions ? (
121+
<SuggestionMenu
122+
items={[...agentSuggestionItems, ...fileSuggestionItems]}
123+
selectedIndex={agentSelectedIndex}
124+
maxVisible={10}
125+
prefix="@"
126+
/>
127+
) : null}
128+
<box
129+
style={{
130+
flexDirection: 'column',
131+
justifyContent: shouldCenterInputVertically
132+
? 'center'
133+
: 'flex-start',
134+
minHeight: shouldCenterInputVertically ? 3 : undefined,
135+
gap: 0,
136+
}}
137+
>
138+
<box
139+
style={{
140+
flexDirection: 'row',
141+
alignItems: shouldCenterInputVertically
142+
? 'center'
143+
: 'flex-start',
144+
width: '100%',
145+
}}
146+
>
147+
<box style={{ flexGrow: 1, minWidth: 0 }}>
148+
<MultilineInput
149+
value={inputValue}
150+
onChange={setInputValue}
151+
onSubmit={handleSubmit}
152+
placeholder={inputPlaceholder}
153+
focused={inputFocused && !feedbackMode}
154+
maxHeight={Math.floor(terminalHeight / 2)}
155+
width={inputWidth}
156+
onKeyIntercept={handleSuggestionMenuKey}
157+
textAttributes={theme.messageTextAttributes}
158+
ref={inputRef}
159+
cursorPosition={cursorPosition}
160+
/>
161+
</box>
162+
<box
163+
style={{
164+
flexShrink: 0,
165+
paddingLeft: 2,
166+
}}
167+
>
168+
<AgentModeToggle
169+
mode={agentMode}
170+
onToggle={toggleAgentMode}
171+
onSelectMode={setAgentMode}
172+
/>
173+
</box>
174+
</box>
175+
</box>
176+
</box>
177+
<UsageBanner />
178+
</>
179+
)
180+
}

0 commit comments

Comments
 (0)