Skip to content

Commit f368a2f

Browse files
Add agent color styling and session persistence
1 parent 13ebfc1 commit f368a2f

5 files changed

Lines changed: 253 additions & 85 deletions

File tree

frontend/src/components/agent/AgentQuickSelect.tsx

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DropdownMenuTrigger,
1010
} from '@/components/ui/dropdown-menu'
1111
import { useAgents } from '@/hooks/useOpenCode'
12+
import { getAgentStyleVars } from '@/lib/agent-colors'
1213

1314
interface AgentQuickSelectProps {
1415
opcodeUrl: string | null | undefined
@@ -19,33 +20,33 @@ interface AgentQuickSelectProps {
1920
disabled?: boolean
2021
}
2122

22-
const getAgentStyles = (agent: string) => {
23-
const lowerAgent = agent.toLowerCase()
24-
if (lowerAgent === 'plan') {
25-
return {
26-
color: 'text-yellow-600 dark:text-yellow-500',
27-
bg: 'bg-yellow-500/20 border-yellow-400 hover:bg-yellow-500/30 hover:border-yellow-300',
28-
shadow: 'shadow-yellow-500/20 hover:shadow-yellow-500/30',
29-
}
30-
}
31-
if (lowerAgent === 'build') {
32-
return {
33-
color: 'text-green-600 dark:text-green-500',
34-
bg: 'bg-green-500/20 border-green-400 hover:bg-green-500/30 hover:border-green-300',
35-
shadow: 'shadow-green-500/20 hover:shadow-green-500/30',
36-
}
37-
}
38-
return {
39-
color: 'text-blue-600 dark:text-blue-500',
40-
bg: 'bg-blue-500/20 border-blue-400 hover:bg-blue-500/30 hover:border-blue-300',
41-
shadow: 'shadow-blue-500/20 hover:shadow-blue-500/30',
42-
}
23+
interface AgentInfo {
24+
name: string
25+
color?: string
26+
description?: string
27+
mode?: string
28+
hidden?: boolean
29+
}
30+
31+
const findAgentColor = (agents: AgentInfo[], agentName: string): string | undefined => {
32+
return agents.find(a => a.name.toLowerCase() === agentName.toLowerCase())?.color
4333
}
4434

45-
const bashStyles = {
46-
color: 'text-purple-700 dark:text-purple-300',
47-
bg: 'bg-purple-500/20 border-purple-400',
48-
shadow: 'shadow-purple-500/20 hover:shadow-purple-500/30',
35+
const bashStyleVars: Record<string, string> = {
36+
'--agent-color-light': '#a753ae',
37+
'--agent-color-dark': '#edb2f1',
38+
'--agent-bg-light': 'rgba(167, 83, 174, 0.2)',
39+
'--agent-bg-dark': 'rgba(237, 178, 241, 0.2)',
40+
'--agent-bg-hover-light': 'rgba(167, 83, 174, 0.3)',
41+
'--agent-bg-hover-dark': 'rgba(237, 178, 241, 0.3)',
42+
'--agent-border-light': 'rgba(167, 83, 174, 0.6)',
43+
'--agent-border-dark': 'rgba(237, 178, 241, 0.6)',
44+
'--agent-border-hover-light': 'rgba(167, 83, 174, 0.5)',
45+
'--agent-border-hover-dark': 'rgba(237, 178, 241, 0.5)',
46+
'--agent-shadow-light': 'rgba(167, 83, 174, 0.2)',
47+
'--agent-shadow-dark': 'rgba(237, 178, 241, 0.2)',
48+
'--agent-shadow-hover-light': 'rgba(167, 83, 174, 0.3)',
49+
'--agent-shadow-hover-dark': 'rgba(237, 178, 241, 0.3)',
4950
}
5051

5152
export function AgentQuickSelect({
@@ -66,60 +67,52 @@ export function AgentQuickSelect({
6667
)
6768
}, [agents])
6869

69-
const handleToggle = () => {
70-
if (primaryAgents.length === 0) return
71-
72-
const currentIndex = primaryAgents.findIndex(
73-
(a) => a.name.toLowerCase() === currentAgent.toLowerCase()
74-
)
75-
const nextIndex = (currentIndex + 1) % primaryAgents.length
76-
onAgentChange(primaryAgents[nextIndex].name)
77-
}
78-
7970
const handleSelect = (agentName: string) => {
8071
onAgentChange(agentName)
8172
}
8273

83-
const styles = isBashMode ? bashStyles : getAgentStyles(currentAgent)
74+
const styleVars = isBashMode
75+
? bashStyleVars
76+
: getAgentStyleVars(currentAgent, findAgentColor(agents, currentAgent))
8477
const displayName = isBashMode ? 'Bash' : capitalize(currentAgent)
8578

8679
const buttonContent = (
8780
<button
8881
data-toggle-mode
89-
onClick={primaryAgents.length <= 2 ? handleToggle : undefined}
9082
disabled={disabled}
91-
className={`px-2 md:px-3.5 py-1 h-[36px] rounded-lg text-sm font-medium border min-w-[48px] max-w-[80px] md:max-w-[100px] flex items-center justify-center transition-all duration-200 active:scale-95 hover:scale-105 shadow-md ${styles.bg} ${styles.color} ${styles.shadow}`}
83+
style={styleVars as React.CSSProperties}
84+
className="px-2 md:px-3.5 py-1 h-[36px] rounded-lg text-sm font-medium border min-w-[56px] max-w-[80px] md:max-w-[100px] flex-shrink-0 flex items-center justify-center transition-all duration-200 active:scale-95 hover:scale-105 shadow-md text-[var(--agent-color-light)] dark:text-[var(--agent-color-dark)] bg-[var(--agent-bg-light)] dark:bg-[var(--agent-bg-dark)] border-[var(--agent-border-light)] dark:border-[var(--agent-border-dark)] hover:bg-[var(--agent-bg-hover-light)] dark:hover:bg-[var(--agent-bg-hover-dark)] hover:border-[var(--agent-border-hover-light)] dark:hover:border-[var(--agent-border-hover-dark)] shadow-[var(--agent-shadow-light)] dark:shadow-[var(--agent-shadow-dark)] hover:shadow-[var(--agent-shadow-hover-light)] dark:hover:shadow-[var(--agent-shadow-hover-dark)]"
9285
>
9386
<span className="truncate">{displayName}</span>
9487
</button>
9588
)
9689

97-
if (primaryAgents.length <= 2) {
98-
return buttonContent
99-
}
100-
10190
return (
10291
<DropdownMenu>
10392
<DropdownMenuTrigger asChild disabled={disabled}>
10493
{buttonContent}
10594
</DropdownMenuTrigger>
106-
<DropdownMenuContent align="start" className="w-48">
95+
<DropdownMenuContent align="start" className="w-64">
10796
{primaryAgents.map((agent) => {
108-
const agentStyles = getAgentStyles(agent.name)
97+
const apiColor = agent.color
98+
const itemStyleVars = getAgentStyleVars(agent.name, apiColor)
10999
const isSelected = agent.name.toLowerCase() === currentAgent.toLowerCase()
110100

111101
return (
112102
<DropdownMenuItem
113103
key={agent.name}
114104
onClick={() => handleSelect(agent.name)}
115-
className="flex items-center justify-between"
105+
className="group flex items-center justify-between"
116106
>
117-
<div className="flex flex-col">
118-
<span className={`font-medium ${agentStyles.color}`}>
107+
<div className="flex flex-col min-w-0">
108+
<span
109+
className="font-medium"
110+
style={{ color: itemStyleVars['--agent-color-light'] }}
111+
>
119112
{capitalize(agent.name)}
120113
</span>
121114
{agent.description && (
122-
<span className="text-xs text-muted-foreground truncate max-w-[160px]">
115+
<span className="text-xs text-muted-foreground line-clamp-2 group-hover:line-clamp-none">
123116
{agent.description}
124117
</span>
125118
)}

frontend/src/components/message/PromptInput.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useSessionAgent } from '@/hooks/useSessionAgent'
99
import { useSTT } from '@/hooks/useSTT'
1010

1111
import { useUserBash } from '@/stores/userBashStore'
12+
import { useSessionAgentStore } from '@/stores/sessionAgentStore'
1213
import { useMobile } from '@/hooks/useMobile'
1314

1415
import { usePermissions } from '@/contexts/EventContext'
@@ -193,13 +194,15 @@ export const PromptInput = memo(forwardRef<PromptInputHandle, PromptInputProps>(
193194

194195
if (hasActiveStream) {
195196
const parts = parsePromptToParts(prompt, attachedFiles, imageAttachments)
197+
const agentUsed = selectedAgent || currentMode
196198
sendPrompt.mutate({
197199
sessionID,
198200
parts,
199201
model: currentModel,
200-
agent: selectedAgent || currentMode,
202+
agent: agentUsed,
201203
variant: currentVariant
202204
})
205+
setStoredAgent(sessionID, agentUsed)
203206
setPrompt('')
204207
setAttachedFiles(new Map())
205208
revokeBlobUrls(imageAttachments)
@@ -217,6 +220,7 @@ export const PromptInput = memo(forwardRef<PromptInputHandle, PromptInputProps>(
217220
command,
218221
agent: currentMode
219222
})
223+
setStoredAgent(sessionID, currentMode)
220224
setPrompt('')
221225
setIsBashMode(false)
222226
clearSTT()
@@ -239,15 +243,17 @@ export const PromptInput = memo(forwardRef<PromptInputHandle, PromptInputProps>(
239243
}
240244

241245
const parts = parsePromptToParts(prompt, attachedFiles, imageAttachments)
246+
const agentUsed = selectedAgent || currentMode
242247

243248
sendPrompt.mutate({
244249
sessionID,
245250
parts,
246251
model: currentModel,
247-
agent: selectedAgent || currentMode,
252+
agent: agentUsed,
248253
variant: currentVariant
249254
})
250255

256+
setStoredAgent(sessionID, agentUsed)
251257
setPrompt('')
252258
setAttachedFiles(new Map())
253259
revokeBlobUrls(imageAttachments)
@@ -356,6 +362,7 @@ export const PromptInput = memo(forwardRef<PromptInputHandle, PromptInputProps>(
356362

357363
const handleAgentChange = (agent: string) => {
358364
setLocalMode(agent)
365+
setStoredAgent(sessionID, agent)
359366
}
360367

361368
const handleVoiceToggle = async () => {
@@ -718,6 +725,7 @@ if (isIOS && isSecureContext && navigator.clipboard && navigator.clipboard.read)
718725

719726
const sessionAgent = useSessionAgent(opcodeUrl, sessionID, directory)
720727
const currentMode = localMode ?? sessionAgent.agent
728+
const setStoredAgent = useSessionAgentStore((s) => s.setAgent)
721729

722730
const { model, modelString } = useModelSelection(opcodeUrl, directory)
723731
const currentModel = modelString || ''
@@ -806,7 +814,7 @@ return (
806814
)}
807815

808816
<div className="flex gap-1.5 md:gap-2 items-center justify-between">
809-
<div className="flex gap-1.5 md:gap-2 items-center">
817+
<div className="flex gap-1.5 md:gap-2 items-center min-w-0">
810818
<AgentQuickSelect
811819
opcodeUrl={opcodeUrl}
812820
directory={directory}
Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useMemo, useRef } from 'react'
1+
import { useMemo, useRef, useEffect } from 'react'
22
import { useMessages } from './useOpenCode'
3+
import { useSessionAgentStore } from '@/stores/sessionAgentStore'
34
import type { components } from '@/api/opencode-types'
45

56
type UserMessage = components['schemas']['UserMessage']
67

7-
const DEFAULT_AGENT = 'plan'
8+
const DEFAULT_AGENT = 'build'
89

910
interface SessionAgentResult {
1011
agent: string
@@ -17,56 +18,102 @@ export function useSessionAgent(
1718
sessionID: string | undefined,
1819
directory?: string
1920
) {
20-
const { data: messages } = useMessages(opcodeUrl, sessionID, directory)
21+
const { data: messages, isLoading: messagesLoading } = useMessages(opcodeUrl, sessionID, directory)
22+
const storedAgent = useSessionAgentStore((s) => s.agents[sessionID ?? ''] ?? null)
23+
const setAgent = useSessionAgentStore((s) => s.setAgent)
2124
const prevRef = useRef<SessionAgentResult>({ agent: DEFAULT_AGENT, model: undefined, variant: undefined })
2225

23-
return useMemo(() => {
24-
let agent = DEFAULT_AGENT
25-
let model: { providerID: string; modelID: string } | undefined
26-
let variant: string | undefined
27-
28-
if (messages && messages.length > 0) {
29-
for (let i = messages.length - 1; i >= 0; i--) {
30-
const msgWithParts = messages[i]
31-
if (msgWithParts.info.role === 'user') {
32-
const userInfo = msgWithParts.info as UserMessage
33-
agent = userInfo.agent || DEFAULT_AGENT
34-
model = userInfo.model
35-
variant = userInfo.variant
36-
break
26+
const result = useMemo(() => {
27+
if (storedAgent) {
28+
let model: { providerID: string; modelID: string } | undefined
29+
let variant: string | undefined
30+
31+
if (messages && messages.length > 0) {
32+
for (let i = messages.length - 1; i >= 0; i--) {
33+
const msgWithParts = messages[i]
34+
if (msgWithParts.info.role === 'user') {
35+
const userInfo = msgWithParts.info as UserMessage
36+
model = userInfo.model
37+
variant = userInfo.variant
38+
break
39+
}
40+
}
41+
}
42+
43+
const prev = prevRef.current
44+
if (
45+
prev.agent === storedAgent &&
46+
prev.variant === variant &&
47+
prev.model?.providerID === model?.providerID &&
48+
prev.model?.modelID === model?.modelID
49+
) {
50+
return prev
51+
}
52+
53+
const next: SessionAgentResult = { agent: storedAgent, model, variant }
54+
prevRef.current = next
55+
return next
56+
}
57+
58+
if (messagesLoading) {
59+
return { agent: DEFAULT_AGENT, model: undefined, variant: undefined }
60+
}
61+
62+
if (!messages || messages.length === 0) {
63+
return { agent: DEFAULT_AGENT, model: undefined, variant: undefined }
64+
}
65+
66+
for (let i = messages.length - 1; i >= 0; i--) {
67+
const msgWithParts = messages[i]
68+
if (msgWithParts.info.role === 'user') {
69+
const userInfo = msgWithParts.info as UserMessage
70+
if (userInfo.agent) {
71+
const prev = prevRef.current
72+
if (
73+
prev.agent === userInfo.agent &&
74+
prev.variant === userInfo.variant &&
75+
prev.model?.providerID === userInfo.model?.providerID &&
76+
prev.model?.modelID === userInfo.model?.modelID
77+
) {
78+
return prev
79+
}
80+
81+
const next: SessionAgentResult = {
82+
agent: userInfo.agent,
83+
model: userInfo.model,
84+
variant: userInfo.variant,
85+
}
86+
prevRef.current = next
87+
return next
3788
}
3889
}
3990
}
4091

41-
const prev = prevRef.current
42-
if (
43-
prev.agent === agent &&
44-
prev.variant === variant &&
45-
prev.model?.providerID === model?.providerID &&
46-
prev.model?.modelID === model?.modelID
47-
) {
48-
return prev
92+
return { agent: DEFAULT_AGENT, model: undefined, variant: undefined }
93+
}, [messages, messagesLoading, storedAgent])
94+
95+
useEffect(() => {
96+
if (result.agent && sessionID) {
97+
setAgent(sessionID, result.agent)
4998
}
99+
}, [result.agent, sessionID, setAgent])
50100

51-
const next: SessionAgentResult = { agent, model, variant }
52-
prevRef.current = next
53-
return next
54-
}, [messages])
101+
return result
55102
}
56103

57104
export function getSessionAgentFromMessages(
58105
messages: Array<{ role: string; agent?: string }> | undefined
59-
): string {
106+
): string | undefined {
60107
if (!messages || messages.length === 0) {
61-
return DEFAULT_AGENT
108+
return undefined
62109
}
63110

64111
for (let i = messages.length - 1; i >= 0; i--) {
65112
const msg = messages[i]
66-
if (msg.role === 'user' && 'agent' in msg) {
67-
return (msg.agent as string) || DEFAULT_AGENT
113+
if (msg.role === 'user' && 'agent' in msg && msg.agent) {
114+
return msg.agent
68115
}
69116
}
70117

71-
return DEFAULT_AGENT
118+
return undefined
72119
}

0 commit comments

Comments
 (0)