Skip to content

Commit c3a7c2f

Browse files
committed
cli: Split out App vs. Chat components
1 parent 6bac2e3 commit c3a7c2f

File tree

4 files changed

+194
-234
lines changed

4 files changed

+194
-234
lines changed

cli/src/app.tsx

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import os from 'os'
2+
import path from 'path'
3+
4+
import { useMemo, useState } from 'react'
5+
6+
import { Chat } from './chat'
7+
import { TerminalLink } from './components/terminal-link'
8+
import { ToolCallItem } from './components/tools/tool-call-item'
9+
import { useLogo } from './hooks/use-logo'
10+
import { useTerminalDimensions } from './hooks/use-terminal-dimensions'
11+
import { useTheme } from './hooks/use-theme'
12+
import { createValidationErrorBlocks } from './utils/create-validation-error-blocks'
13+
import { openFileAtPath } from './utils/open-file'
14+
import { pluralize } from '@codebuff/common/util/string'
15+
16+
interface AppProps {
17+
initialPrompt: string | null
18+
agentId?: string
19+
requireAuth: boolean | null
20+
hasInvalidCredentials: boolean
21+
loadedAgentsData: {
22+
agents: Array<{ id: string; displayName: string }>
23+
agentsDir: string
24+
} | null
25+
validationErrors: Array<{ id: string; message: string }>
26+
}
27+
28+
export const App = ({
29+
initialPrompt,
30+
agentId,
31+
requireAuth,
32+
hasInvalidCredentials,
33+
loadedAgentsData,
34+
validationErrors,
35+
}: AppProps) => {
36+
const { contentMaxWidth, separatorWidth } = useTerminalDimensions()
37+
const theme = useTheme()
38+
const { textBlock: logoBlock } = useLogo({ availableWidth: contentMaxWidth })
39+
40+
const [isAgentListCollapsed, setIsAgentListCollapsed] = useState(true)
41+
42+
const headerContent = useMemo(() => {
43+
if (!loadedAgentsData) {
44+
return null
45+
}
46+
47+
const homeDir = os.homedir()
48+
const repoRoot = path.dirname(loadedAgentsData.agentsDir)
49+
const relativePath = path.relative(homeDir, repoRoot)
50+
const displayPath = relativePath.startsWith('..')
51+
? repoRoot
52+
: `~/${relativePath}`
53+
54+
const sortedAgents = [...loadedAgentsData.agents].sort((a, b) => {
55+
const displayNameComparison = (a.displayName || '')
56+
.toLowerCase()
57+
.localeCompare((b.displayName || '').toLowerCase())
58+
59+
return (
60+
displayNameComparison ||
61+
a.id.toLowerCase().localeCompare(b.id.toLowerCase())
62+
)
63+
})
64+
65+
const agentCount = sortedAgents.length
66+
67+
const formatIdentifier = (agent: { id: string; displayName: string }) =>
68+
agent.displayName && agent.displayName !== agent.id
69+
? `${agent.displayName} (${agent.id})`
70+
: agent.displayName || agent.id
71+
72+
const renderAgentListItem = (
73+
agent: { id: string; displayName: string },
74+
idx: number,
75+
) => {
76+
const identifier = formatIdentifier(agent)
77+
return (
78+
<text
79+
key={`agent-${idx}`}
80+
style={{ wrapMode: 'word', fg: theme.foreground }}
81+
>
82+
{`• ${identifier}`}
83+
</text>
84+
)
85+
}
86+
87+
const agentListContent = (
88+
<box style={{ flexDirection: 'column', gap: 0 }}>
89+
{sortedAgents.map(renderAgentListItem)}
90+
</box>
91+
)
92+
93+
const headerText = pluralize(agentCount, 'local agent')
94+
95+
return (
96+
<box
97+
style={{
98+
flexDirection: 'column',
99+
gap: 0,
100+
paddingLeft: 1,
101+
paddingRight: 1,
102+
}}
103+
>
104+
<text
105+
style={{
106+
wrapMode: 'word',
107+
marginBottom: 1,
108+
marginTop: 2,
109+
fg: theme.foreground,
110+
}}
111+
>
112+
{logoBlock}
113+
</text>
114+
<text
115+
style={{ wrapMode: 'word', marginBottom: 1, fg: theme.foreground }}
116+
>
117+
Codebuff will run commands on your behalf to help you build.
118+
</text>
119+
<text
120+
style={{ wrapMode: 'word', marginBottom: 1, fg: theme.foreground }}
121+
>
122+
Directory{' '}
123+
<TerminalLink
124+
text={displayPath}
125+
inline={true}
126+
underlineOnHover={true}
127+
onActivate={() => openFileAtPath(repoRoot)}
128+
/>
129+
</text>
130+
<box style={{ marginBottom: 1 }}>
131+
<ToolCallItem
132+
name={headerText}
133+
content={agentListContent}
134+
isCollapsed={isAgentListCollapsed}
135+
isStreaming={false}
136+
streamingPreview=""
137+
finishedPreview=""
138+
onToggle={() => setIsAgentListCollapsed(!isAgentListCollapsed)}
139+
dense
140+
/>
141+
</box>
142+
{validationErrors.length > 0 && (
143+
<box style={{ flexDirection: 'column', gap: 0 }}>
144+
{createValidationErrorBlocks({
145+
errors: validationErrors,
146+
loadedAgentsData,
147+
availableWidth: separatorWidth,
148+
}).map((block, idx) => {
149+
if (block.type === 'html') {
150+
return (
151+
<box key={`validation-error-${idx}`}>
152+
{block.render({ textColor: theme.foreground, theme })}
153+
</box>
154+
)
155+
}
156+
return null
157+
})}
158+
</box>
159+
)}
160+
</box>
161+
)
162+
}, [
163+
loadedAgentsData,
164+
logoBlock,
165+
theme,
166+
isAgentListCollapsed,
167+
validationErrors,
168+
separatorWidth,
169+
])
170+
171+
return (
172+
<box style={{ flexDirection: 'column', gap: 0, flexGrow: 1 }}>
173+
<Chat
174+
headerContent={headerContent}
175+
initialPrompt={initialPrompt}
176+
agentId={agentId}
177+
requireAuth={requireAuth}
178+
hasInvalidCredentials={hasInvalidCredentials}
179+
loadedAgentsData={loadedAgentsData}
180+
validationErrors={validationErrors}
181+
/>
182+
</box>
183+
)
184+
}

cli/src/chat.tsx

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { useShallow } from 'zustand/react/shallow'
33

44
import type { ContentBlock } from './types/chat'
@@ -14,22 +14,20 @@ import {
1414
import { StatusIndicator, useHasStatus } from './components/status-indicator'
1515
import { SuggestionMenu } from './components/suggestion-menu'
1616
import { SLASH_COMMANDS } from './data/slash-commands'
17-
import { useAgentInitialization } from './hooks/use-agent-initialization'
1817
import { useAgentValidation } from './hooks/use-agent-validation'
1918
import { useAuthState } from './hooks/use-auth-state'
2019
import { useChatInput } from './hooks/use-chat-input'
2120
import { useClipboard } from './hooks/use-clipboard'
2221
import { useElapsedTime } from './hooks/use-elapsed-time'
2322
import { useInputHistory } from './hooks/use-input-history'
2423
import { useKeyboardHandlers } from './hooks/use-keyboard-handlers'
25-
import { useLogo } from './hooks/use-logo'
2624
import { useMessageQueue } from './hooks/use-message-queue'
2725
import { useMessageRenderer } from './hooks/use-message-renderer'
2826
import { useChatScrollbox } from './hooks/use-scroll-management'
2927
import { useSendMessage } from './hooks/use-send-message'
3028
import { useSuggestionEngine } from './hooks/use-suggestion-engine'
3129
import { useTerminalDimensions } from './hooks/use-terminal-dimensions'
32-
import { useTheme, useResolvedThemeName } from './hooks/use-theme'
30+
import { useTheme } from './hooks/use-theme'
3331
import { useValidationBanner } from './hooks/use-validation-banner'
3432
import { useChatStore } from './state/chat-store'
3533
import { flushAnalytics } from './utils/analytics'
@@ -53,14 +51,16 @@ const DEFAULT_AGENT_IDS = {
5351
PLAN: 'base2-plan',
5452
} as const
5553

56-
export const App = ({
54+
export const Chat = ({
55+
headerContent,
5756
initialPrompt,
5857
agentId,
5958
requireAuth,
6059
hasInvalidCredentials,
6160
loadedAgentsData,
6261
validationErrors,
6362
}: {
63+
headerContent: React.ReactNode
6464
initialPrompt: string | null
6565
agentId?: string
6666
requireAuth: boolean | null
@@ -74,16 +74,12 @@ export const App = ({
7474
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
7575
const inputRef = useRef<MultilineInputHandle | null>(null)
7676

77-
const { terminalWidth, separatorWidth, contentMaxWidth } =
78-
useTerminalDimensions()
77+
const { terminalWidth, separatorWidth } = useTerminalDimensions()
7978

8079
const theme = useTheme()
81-
const resolvedThemeName = useResolvedThemeName()
8280
const markdownPalette = useMemo(() => createMarkdownPalette(theme), [theme])
83-
const { textBlock: logoBlock } = useLogo({ availableWidth: contentMaxWidth })
8481

85-
const { validationErrors: liveValidationErrors, validate: validateAgents } =
86-
useAgentValidation(validationErrors)
82+
const { validate: validateAgents } = useAgentValidation(validationErrors)
8783

8884
const exitWarningTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
8985
null,
@@ -200,19 +196,6 @@ export const App = ({
200196
resetChatStore,
201197
})
202198

203-
useAgentInitialization({
204-
loadedAgentsData,
205-
validationErrors,
206-
logoBlock,
207-
theme,
208-
separatorWidth,
209-
agentId,
210-
resolvedThemeName,
211-
messages,
212-
setMessages,
213-
setCollapsedAgents,
214-
})
215-
216199
const showAgentDisplayName = !!agentId
217200
const agentDisplayName = useMemo(() => {
218201
if (!loadedAgentsData) return null
@@ -796,7 +779,7 @@ export const App = ({
796779
)
797780

798781
const validationBanner = useValidationBanner({
799-
liveValidationErrors,
782+
liveValidationErrors: validationErrors,
800783
loadedAgentsData,
801784
theme,
802785
})
@@ -854,6 +837,7 @@ export const App = ({
854837
},
855838
}}
856839
>
840+
{headerContent}
857841
{virtualizationNotice}
858842
{messageItems}
859843
</scrollbox>

0 commit comments

Comments
 (0)