Skip to content

Commit 32ee36b

Browse files
committed
Refine macOS terminal theming
1 parent edd6f27 commit 32ee36b

File tree

14 files changed

+1043
-258
lines changed

14 files changed

+1043
-258
lines changed

bun.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
},
3434
"dependencies": {
3535
"@codebuff/sdk": "workspace:*",
36-
"@opentui/core": "^0.1.31",
37-
"@opentui/react": "^0.1.31",
36+
"@opentui/core": "0.0.0-20251029-f23e92a5",
37+
"@opentui/react": "0.0.0-20251029-f23e92a5",
3838
"@tanstack/react-query": "^5.62.8",
3939
"commander": "^14.0.1",
4040
"immer": "^10.1.3",

cli/src/chat.tsx

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import { formatQueuedPreview } from './utils/helpers'
3131
import { loadLocalAgents } from './utils/local-agent-registry'
3232
import { logger } from './utils/logger'
3333
import { buildMessageTree } from './utils/message-tree-utils'
34-
import { chatTheme, createMarkdownPalette } from './utils/theme-system'
34+
import {
35+
chatTheme,
36+
createMarkdownPalette,
37+
onThemeChange,
38+
type ChatTheme,
39+
} from './utils/theme-system'
3540

3641
import type { User } from './utils/auth'
3742
import type { ToolName } from '@codebuff/sdk'
@@ -127,8 +132,47 @@ export const App = ({
127132
const terminalWidth = resolvedTerminalWidth
128133
const separatorWidth = Math.max(1, Math.floor(terminalWidth) - 2)
129134

130-
const theme = chatTheme
131-
const markdownPalette = useMemo(() => createMarkdownPalette(theme), [theme])
135+
const cloneTheme = (input: ChatTheme): ChatTheme => ({
136+
...input,
137+
markdown: input.markdown
138+
? {
139+
...input.markdown,
140+
headingFg: input.markdown.headingFg
141+
? { ...input.markdown.headingFg }
142+
: undefined,
143+
}
144+
: undefined,
145+
})
146+
147+
const [theme, setTheme] = useState<ChatTheme>(() => cloneTheme(chatTheme))
148+
const [resolvedThemeName, setResolvedThemeName] = useState<'dark' | 'light'>(
149+
chatTheme.messageTextAttributes ? 'dark' : 'light',
150+
)
151+
152+
useEffect(() => {
153+
const unsubscribe = onThemeChange((updatedTheme, meta) => {
154+
const nextTheme = cloneTheme(updatedTheme)
155+
setTheme(nextTheme)
156+
setResolvedThemeName(meta.resolvedThemeName)
157+
if (process.env.CODEBUFF_THEME_DEBUG === '1') {
158+
logger.debug(
159+
{
160+
themeChange: {
161+
source: meta.source,
162+
resolvedThemeName: meta.resolvedThemeName,
163+
},
164+
},
165+
'Applied theme change in chat component',
166+
)
167+
}
168+
})
169+
return unsubscribe
170+
}, [])
171+
172+
const markdownPalette = useMemo(
173+
() => createMarkdownPalette(theme),
174+
[theme],
175+
)
132176

133177
const [exitWarning, setExitWarning] = useState<string | null>(null)
134178
const exitArmedRef = useRef(false)
@@ -191,16 +235,27 @@ export const App = ({
191235
)
192236
}, [])
193237

194-
// Initialize with loaded agents message
238+
// Initialize and update loaded agents message when theme changes
195239
useEffect(() => {
196-
if (loadedAgentsData && messages.length === 0) {
197-
const agentListId = 'loaded-agents-list'
198-
const userCredentials = getUserCredentials()
199-
const greeting = userCredentials?.name?.trim().length
200-
? `Welcome back, ${userCredentials.name.trim()}!`
201-
: null
202-
203-
const blocks: ContentBlock[] = [
240+
if (!loadedAgentsData) {
241+
return
242+
}
243+
244+
const agentListId = 'loaded-agents-list'
245+
const userCredentials = getUserCredentials()
246+
const greeting = userCredentials?.name?.trim().length
247+
? `Welcome back, ${userCredentials.name.trim()}!`
248+
: null
249+
250+
const baseTextColor =
251+
resolvedThemeName === 'dark'
252+
? '#ffffff'
253+
: theme.chromeText && theme.chromeText !== 'default'
254+
? theme.chromeText
255+
: theme.agentResponseCount
256+
257+
const buildBlocks = (listId: string): ContentBlock[] => {
258+
const result: ContentBlock[] = [
204259
{
205260
type: 'text',
206261
content: '\n\n' + LOGO_BLOCK,
@@ -209,41 +264,76 @@ export const App = ({
209264
]
210265

211266
if (greeting) {
212-
blocks.push({
267+
result.push({
213268
type: 'text',
214269
content: greeting,
215-
color: theme.agentResponseCount,
270+
color: baseTextColor,
216271
})
217272
}
218273

219-
blocks.push(
274+
result.push(
220275
{
221276
type: 'text',
222277
content:
223278
'Codebuff can read and write files in this repository, and run terminal commands to help you build.',
224-
color: theme.agentResponseCount,
279+
color: baseTextColor,
225280
},
226281
{
227282
type: 'agent-list',
228-
id: agentListId,
283+
id: listId,
229284
agents: loadedAgentsData.agents,
230285
agentsDir: loadedAgentsData.agentsDir,
231286
},
232287
)
233288

289+
return result
290+
}
291+
292+
if (messages.length === 0) {
293+
const initialBlocks = buildBlocks(agentListId)
234294
const initialMessage: ChatMessage = {
235295
id: `system-loaded-agents-${Date.now()}`,
236296
variant: 'ai',
237297
content: '', // Content is in the block
238-
blocks,
298+
blocks: initialBlocks,
239299
timestamp: new Date().toISOString(),
240300
}
241301

242-
// Set as collapsed by default
243302
setCollapsedAgents((prev) => new Set([...prev, agentListId]))
244303
setMessages([initialMessage])
304+
return
245305
}
246-
}, [loadedAgentsData, theme]) // Only run when loadedAgentsData changes
306+
307+
setMessages((prev) => {
308+
if (prev.length === 0) {
309+
return prev
310+
}
311+
312+
const [firstMessage, ...rest] = prev
313+
if (!firstMessage.blocks) {
314+
return prev
315+
}
316+
317+
const agentListBlock = firstMessage.blocks.find(
318+
(block): block is Extract<ContentBlock, { type: 'agent-list' }> =>
319+
block.type === 'agent-list',
320+
)
321+
322+
if (!agentListBlock) {
323+
return prev
324+
}
325+
326+
const updatedBlocks = buildBlocks(agentListBlock.id)
327+
328+
return [
329+
{
330+
...firstMessage,
331+
blocks: updatedBlocks,
332+
},
333+
...rest,
334+
]
335+
})
336+
}, [loadedAgentsData, resolvedThemeName, theme])
247337

248338
const {
249339
inputValue,

cli/src/components/branch-item.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ export const BranchItem = ({
5151
onToggle,
5252
}: BranchItemProps) => {
5353
const resolveFg = (
54-
color?: string,
55-
fallback?: string,
54+
color?: string | null,
55+
fallback?: string | null,
5656
): string | undefined => {
5757
if (color && color !== 'default') return color
5858
if (fallback && fallback !== 'default') return fallback
@@ -63,14 +63,20 @@ export const BranchItem = ({
6363
resolveFg(theme.chromeText) ??
6464
'#d1d5e5'
6565

66+
const baseTextAttributes = theme.messageTextAttributes ?? 0
67+
const getAttributes = (extra: number = 0): number | undefined => {
68+
const combined = baseTextAttributes | extra
69+
return combined === 0 ? undefined : combined
70+
}
71+
6672
const isExpanded = !isCollapsed
6773
const toggleFrameColor = isExpanded
6874
? theme.agentToggleExpandedBg
69-
: theme.agentToggleHeaderBg
75+
: theme.agentResponseCount ?? theme.agentToggleHeaderBg
7076
const toggleIconColor = isStreaming
7177
? theme.statusAccent
72-
: toggleFrameColor
73-
const toggleLabelColor = toggleFrameColor
78+
: theme.chromeText ?? toggleFrameColor
79+
const toggleLabelColor = theme.chromeText ?? toggleFrameColor
7480
const toggleLabel = `${isCollapsed ? '▸' : '▾'} `
7581
const collapseButtonFrame = theme.agentToggleExpandedBg
7682
const collapseButtonText = collapseButtonFrame
@@ -131,7 +137,11 @@ export const BranchItem = ({
131137

132138
if (isTextRenderable(value)) {
133139
return (
134-
<text fg={theme.agentText} key="expanded-text">
140+
<text
141+
fg={resolveFg(theme.agentText)}
142+
key="expanded-text"
143+
attributes={getAttributes()}
144+
>
135145
{value}
136146
</text>
137147
)
@@ -209,7 +219,11 @@ export const BranchItem = ({
209219
}}
210220
>
211221
<text {...(headerFg ? { fg: headerFg } : undefined)}>Prompt</text>
212-
<text fg={theme.agentText} style={{ wrapMode: 'word' }}>
222+
<text
223+
fg={resolveFg(theme.agentText)}
224+
style={{ wrapMode: 'word' }}
225+
attributes={getAttributes()}
226+
>
213227
{prompt}
214228
</text>
215229
</box>
@@ -254,12 +268,15 @@ export const BranchItem = ({
254268
paddingLeft: 1,
255269
paddingRight: 1,
256270
paddingTop: 0,
257-
paddingBottom: 1,
271+
paddingBottom: 1,
258272
}}
259273
>
260274
<text
261-
fg={isStreaming ? theme.agentText : theme.agentResponseCount}
262-
attributes={TextAttributes.ITALIC}
275+
fg={resolveFg(
276+
isStreaming ? theme.agentText : theme.agentResponseCount,
277+
fallbackTextColor,
278+
)}
279+
attributes={getAttributes(TextAttributes.ITALIC)}
263280
>
264281
{isStreaming ? streamingPreview : finishedPreview}
265282
</text>
@@ -285,7 +302,11 @@ export const BranchItem = ({
285302
}}
286303
>
287304
<text {...(headerFg ? { fg: headerFg } : undefined)}>Prompt</text>
288-
<text fg={theme.agentText} style={{ wrapMode: 'word' }}>
305+
<text
306+
fg={resolveFg(theme.agentText)}
307+
style={{ wrapMode: 'word' }}
308+
attributes={getAttributes()}
309+
>
289310
{prompt}
290311
</text>
291312
{content && (

cli/src/components/login-modal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { copyTextToClipboard } from '../utils/clipboard'
3232
import { logger } from '../utils/logger'
3333

3434
import type { User } from '../utils/auth'
35-
import type { ChatTheme } from '../utils/theme-system'
35+
import { resolveThemeColor, type ChatTheme } from '../utils/theme-system'
3636

3737
interface LoginModalProps {
3838
onLoginSuccess: (user: User) => void
@@ -221,7 +221,9 @@ export const LoginModal = ({
221221
}
222222
}, [hasOpenedBrowser, loginUrl, copyToClipboard])
223223

224-
const logoColor = theme.chromeText
224+
const logoColor =
225+
resolveThemeColor(theme.chromeText, theme.statusAccent) ??
226+
theme.statusAccent
225227

226228
// Use custom hook for sheen animation
227229
const { applySheenToChar } = useSheenAnimation({
@@ -357,7 +359,7 @@ export const LoginModal = ({
357359
>
358360
<text style={{ wrapMode: 'none' }}>
359361
<b>
360-
<span fg={theme.chromeText}>
362+
<span fg={resolveThemeColor(theme.chromeText, logoColor)}>
361363
{isNarrow ? 'Codebuff' : 'Codebuff CLI'}
362364
</span>
363365
</b>

0 commit comments

Comments
 (0)