Skip to content

Commit 351cc20

Browse files
committed
Show git root notice in top banner
1 parent c28f64b commit 351cc20

File tree

6 files changed

+156
-20
lines changed

6 files changed

+156
-20
lines changed

cli/src/app.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NetworkError, RETRYABLE_ERROR_CODES } from '@codebuff/sdk'
2-
import { useCallback, useMemo, useRef, useState } from 'react'
2+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import { useShallow } from 'zustand/react/shallow'
44

55
import { Chat } from './chat'
@@ -14,9 +14,10 @@ import { useTerminalDimensions } from './hooks/use-terminal-dimensions'
1414
import { useTerminalFocus } from './hooks/use-terminal-focus'
1515
import { useTheme } from './hooks/use-theme'
1616
import { getProjectRoot } from './project-files'
17-
import { useChatStore } from './state/chat-store'
17+
import { useChatStore, type TopBannerType } from './state/chat-store'
1818
import { openFileAtPath } from './utils/open-file'
1919
import { formatCwd } from './utils/path-helpers'
20+
import { findGitRoot } from './utils/git'
2021
import { getLogoBlockColor, getLogoAccentColor } from './utils/theme-system'
2122

2223
import type { MultilineInputHandle } from './components/multiline-input'
@@ -73,11 +74,21 @@ export const App = ({
7374
})
7475

7576
const inputRef = useRef<MultilineInputHandle | null>(null)
76-
const { setInputFocused, setIsFocusSupported, resetChatStore } = useChatStore(
77+
const {
78+
setInputFocused,
79+
setIsFocusSupported,
80+
resetChatStore,
81+
activeTopBanner,
82+
setActiveTopBanner,
83+
closeTopBanner,
84+
} = useChatStore(
7785
useShallow((store) => ({
7886
setInputFocused: store.setInputFocused,
7987
setIsFocusSupported: store.setIsFocusSupported,
8088
resetChatStore: store.reset,
89+
activeTopBanner: store.activeTopBanner,
90+
setActiveTopBanner: store.setActiveTopBanner,
91+
closeTopBanner: store.closeTopBanner,
8192
})),
8293
)
8394

@@ -110,6 +121,53 @@ export const App = ({
110121
})
111122

112123
const projectRoot = getProjectRoot()
124+
const gitRoot = useMemo(
125+
() => findGitRoot({ cwd: projectRoot }),
126+
[projectRoot],
127+
)
128+
const showGitRootBanner = Boolean(gitRoot && gitRoot !== projectRoot)
129+
const [gitRootBannerDismissed, setGitRootBannerDismissed] = useState(false)
130+
const prevTopBannerRef = useRef<TopBannerType | null>(null)
131+
132+
useEffect(() => {
133+
setGitRootBannerDismissed(false)
134+
}, [projectRoot])
135+
136+
useEffect(() => {
137+
const prevBanner = prevTopBannerRef.current
138+
if (
139+
prevBanner === 'gitRoot' &&
140+
activeTopBanner === null &&
141+
showGitRootBanner
142+
) {
143+
setGitRootBannerDismissed(true)
144+
}
145+
prevTopBannerRef.current = activeTopBanner
146+
}, [activeTopBanner, showGitRootBanner])
147+
148+
useEffect(() => {
149+
if (!showGitRootBanner) {
150+
if (activeTopBanner === 'gitRoot') {
151+
closeTopBanner()
152+
}
153+
return
154+
}
155+
if (!gitRootBannerDismissed && activeTopBanner === null) {
156+
setActiveTopBanner('gitRoot')
157+
}
158+
}, [
159+
activeTopBanner,
160+
closeTopBanner,
161+
gitRootBannerDismissed,
162+
setActiveTopBanner,
163+
showGitRootBanner,
164+
])
165+
166+
const handleSwitchToGitRoot = useCallback(() => {
167+
if (gitRoot) {
168+
onProjectChange(gitRoot)
169+
}
170+
}, [gitRoot, onProjectChange])
113171

114172
const headerContent = useMemo(() => {
115173
const displayPath = formatCwd(projectRoot)
@@ -211,6 +269,8 @@ export const App = ({
211269
continueChatId={continueChatId}
212270
authStatus={authStatus}
213271
initialMode={initialMode}
272+
gitRoot={gitRoot}
273+
onSwitchToGitRoot={handleSwitchToGitRoot}
214274
/>
215275
)
216276
}

cli/src/chat.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ export const Chat = ({
9595
continueChatId,
9696
authStatus,
9797
initialMode,
98+
gitRoot,
99+
onSwitchToGitRoot,
98100
}: {
99101
headerContent: React.ReactNode
100102
initialPrompt: string | null
@@ -108,6 +110,8 @@ export const Chat = ({
108110
continueChatId?: string
109111
authStatus: AuthStatus
110112
initialMode?: AgentMode
113+
gitRoot?: string | null
114+
onSwitchToGitRoot?: () => void
111115
}) => {
112116
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
113117
const [hasOverflow, setHasOverflow] = useState(false)
@@ -1368,7 +1372,7 @@ export const Chat = ({
13681372
},
13691373
}}
13701374
>
1371-
<TopBanner />
1375+
<TopBanner gitRoot={gitRoot} onSwitchToGitRoot={onSwitchToGitRoot} />
13721376

13731377
{headerContent}
13741378
{hiddenMessageCount > 0 && (

cli/src/components/top-banner.tsx

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
import React from 'react'
22

33
import { Button } from './button'
4+
import { TerminalLink } from './terminal-link'
45
import { useTheme } from '../hooks/use-theme'
56
import { useChatStore, type TopBannerType } from '../state/chat-store'
7+
import { formatCwd } from '../utils/path-helpers'
68
import { BORDER_CHARS } from '../utils/ui-constants'
79

810
import type { ThemeColorKey, InputMode } from '../utils/input-modes'
11+
import type { ChatTheme } from '../types/theme-system'
12+
13+
type BannerContentParams = {
14+
gitRoot?: string | null
15+
onSwitchToGitRoot?: () => void
16+
textColor: string
17+
theme: ChatTheme
18+
}
919

1020
type BannerConfig = {
1121
/** Theme color key for the border */
1222
borderColorKey: ThemeColorKey
1323
/** Theme color key for text */
1424
textColorKey: ThemeColorKey
1525
/** Banner content */
16-
content: React.ReactNode
26+
content: React.ReactNode | ((params: BannerContentParams) => React.ReactNode)
27+
/** Layout style for content */
28+
layout?: 'text' | 'custom'
1729
/** Input mode to reset to when closing, if currently in a related mode */
1830
relatedInputMode?: InputMode
1931
}
@@ -38,13 +50,43 @@ const TOP_BANNER_REGISTRY: Record<NonNullable<TopBannerType>, BannerConfig> = {
3850
</>
3951
),
4052
},
53+
gitRoot: {
54+
borderColorKey: 'warning',
55+
textColorKey: 'foreground',
56+
layout: 'custom',
57+
content: ({ gitRoot, onSwitchToGitRoot, textColor }) => {
58+
const displayGitRoot = gitRoot ? formatCwd(gitRoot) : 'git root'
59+
return (
60+
<>
61+
<text style={{ wrapMode: 'word', fg: textColor }}>
62+
You started Codebuff in a subdirectory of a git repo.
63+
</text>
64+
{gitRoot && onSwitchToGitRoot ? (
65+
<TerminalLink
66+
text={`Switch to git root (${displayGitRoot})`}
67+
onActivate={onSwitchToGitRoot}
68+
underlineOnHover={true}
69+
lineWrap={true}
70+
containerStyle={{ alignItems: 'flex-start' }}
71+
/>
72+
) : null}
73+
</>
74+
)
75+
},
76+
},
4177
}
4278

4379
/**
4480
* Centralized component for rendering top banners.
4581
* Handles all banner types with consistent styling and behavior.
4682
*/
47-
export const TopBanner = () => {
83+
export const TopBanner = ({
84+
gitRoot,
85+
onSwitchToGitRoot,
86+
}: {
87+
gitRoot?: string | null
88+
onSwitchToGitRoot?: () => void
89+
}) => {
4890
const theme = useTheme()
4991
const activeTopBanner = useChatStore((state) => state.activeTopBanner)
5092
const closeTopBanner = useChatStore((state) => state.closeTopBanner)
@@ -71,6 +113,44 @@ export const TopBanner = () => {
71113
const themeRecord = theme as unknown as Record<string, string>
72114
const borderColor = themeRecord[config.borderColorKey]
73115
const textColor = themeRecord[config.textColorKey]
116+
const contentParams: BannerContentParams = {
117+
gitRoot,
118+
onSwitchToGitRoot,
119+
textColor,
120+
theme,
121+
}
122+
const content =
123+
typeof config.content === 'function'
124+
? config.content(contentParams)
125+
: config.content
126+
127+
if (!content) {
128+
return null
129+
}
130+
131+
const contentNode =
132+
config.layout === 'custom' ? (
133+
<box
134+
style={{
135+
flexDirection: 'column',
136+
flexShrink: 1,
137+
marginRight: 3,
138+
}}
139+
>
140+
{content}
141+
</box>
142+
) : (
143+
<text
144+
style={{
145+
fg: textColor,
146+
wrapMode: 'word',
147+
flexShrink: 1,
148+
marginRight: 3,
149+
}}
150+
>
151+
{content}
152+
</text>
153+
)
74154

75155
return (
76156
<box
@@ -92,16 +172,7 @@ export const TopBanner = () => {
92172
border={['top', 'bottom', 'left', 'right']}
93173
customBorderChars={BORDER_CHARS}
94174
>
95-
<text
96-
style={{
97-
fg: textColor,
98-
wrapMode: 'word',
99-
flexShrink: 1,
100-
marginRight: 3,
101-
}}
102-
>
103-
{config.content}
104-
</text>
175+
{contentNode}
105176
<Button onClick={handleClose}>
106177
<text style={{ fg: borderColor }}>x</text>
107178
</Button>

cli/src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { initializeApp } from './init/init-app'
2222
import { getProjectRoot, setProjectRoot } from './project-files'
2323
import { initAnalytics } from './utils/analytics'
2424
import { getAuthTokenDetails } from './utils/auth'
25+
import { resetCodebuffClient } from './utils/codebuff-client'
2526
import { getCliEnv } from './utils/env'
2627
import { initializeAgentRegistry } from './utils/local-agent-registry'
2728
import { clearLogFile, logger } from './utils/logger'
@@ -273,6 +274,8 @@ async function main(): Promise<void> {
273274
process.chdir(newProjectPath)
274275
// Update the project root in the module state
275276
setProjectRoot(newProjectPath)
277+
// Reset client to ensure tools use the updated project root
278+
resetCodebuffClient()
276279
// Save to recent projects list
277280
saveRecentProject(newProjectPath)
278281
// Update local state

cli/src/init/init-app.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { enableMapSet } from 'immer'
22

33
import { initializeThemeStore } from '../hooks/use-theme'
44
import { setProjectRoot } from '../project-files'
5-
import { findGitRoot } from '../utils/git'
65
import { initTimestampFormatter } from '../utils/helpers'
76
import { enableManualThemeRefresh } from '../utils/theme-system'
87

@@ -13,8 +12,7 @@ export async function initializeApp(params: {
1312
process.chdir(params.cwd)
1413
}
1514
const baseCwd = process.cwd()
16-
const projectRoot = findGitRoot({ cwd: baseCwd }) ?? baseCwd
17-
setProjectRoot(projectRoot)
15+
setProjectRoot(baseCwd)
1816

1917
enableMapSet()
2018
initializeThemeStore()

cli/src/state/chat-store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { InputMode } from '../utils/input-modes'
1212
import type { RunState } from '@codebuff/sdk'
1313

1414
/** Types of banners that can appear at the top of the chat */
15-
export type TopBannerType = 'homeDir' | null
15+
export type TopBannerType = 'homeDir' | 'gitRoot' | null
1616

1717
export type InputValue = {
1818
text: string

0 commit comments

Comments
 (0)