Skip to content

Commit af29275

Browse files
chore: bump version to 0.7.4
1 parent e6156f3 commit af29275

11 files changed

Lines changed: 474 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.7.4] - 2026-01-24
9+
10+
### Features
11+
- Add OpenCode version selection dialog with version fetching from GitHub API
12+
- Add endpoint to fetch available OpenCode versions
13+
- Add endpoint to install and switch OpenCode versions
14+
- Improve agent configuration management with version control UI
15+
16+
### Bug Fixes
17+
- Agent configuration changes now trigger OpenCode server restart instead of live config patch
18+
- Fix Dockerfile to use latest pnpm version instead of pinned 9.15.0
19+
20+
### Other
821
## [0.7.2] - 2026-01-23
922

1023
### Bug Fixes

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | d
2626
&& apt-get update && apt-get install -y gh \
2727
&& rm -rf /var/lib/apt/lists/*
2828

29-
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
29+
RUN corepack enable && corepack prepare pnpm@latest --activate
3030

3131
RUN curl -fsSL https://bun.sh/install | bash && \
3232
mv /root/.bun /opt/bun && \

backend/src/routes/settings.ts

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ export function createSettingsRoutes(db: Database) {
180180
const body = await c.req.json()
181181
const validated = UpdateOpenCodeConfigSchema.parse(body)
182182

183+
const existingConfig = settingsService.getOpenCodeConfigByName(configName, userId)
184+
const existingAgents = existingConfig?.content?.agent
185+
183186
const config = settingsService.updateOpenCodeConfig(configName, validated, userId)
184187
if (!config) {
185188
return c.json({ error: 'Config not found' }, 404)
@@ -190,9 +193,17 @@ export function createSettingsRoutes(db: Database) {
190193
await writeFileContent(configPath, config.rawContent)
191194
logger.info(`Wrote default config to: ${configPath}`)
192195

193-
const patchResult = await patchOpenCodeConfig(config.content)
194-
if (!patchResult.success) {
195-
return c.json({ error: 'Config saved but failed to apply', details: patchResult.error }, 500)
196+
const newAgents = config.content?.agent
197+
const agentsChanged = JSON.stringify(existingAgents) !== JSON.stringify(newAgents)
198+
199+
if (agentsChanged) {
200+
logger.info('Agent configuration changed, restarting OpenCode server')
201+
await opencodeServerManager.restart()
202+
} else {
203+
const patchResult = await patchOpenCodeConfig(config.content)
204+
if (!patchResult.success) {
205+
return c.json({ error: 'Config saved but failed to apply', details: patchResult.error }, 500)
206+
}
196207
}
197208
}
198209

@@ -415,6 +426,92 @@ export function createSettingsRoutes(db: Database) {
415426
}
416427
})
417428

429+
app.get('/opencode-versions', async (c) => {
430+
try {
431+
logger.info('Fetching available OpenCode versions from GitHub')
432+
433+
const response = await fetch('https://api.github.com/repos/sst/opencode/releases?per_page=20', {
434+
headers: {
435+
'Accept': 'application/vnd.github.v3+json',
436+
'User-Agent': 'opencode-manager'
437+
}
438+
})
439+
440+
if (!response.ok) {
441+
throw new Error(`GitHub API returned ${response.status}`)
442+
}
443+
444+
const releases = await response.json() as Array<{
445+
tag_name: string
446+
name: string
447+
published_at: string
448+
prerelease: boolean
449+
}>
450+
451+
const versions = releases
452+
.filter(r => !r.prerelease)
453+
.map(r => ({
454+
version: r.tag_name.replace(/^v/, ''),
455+
tag: r.tag_name,
456+
name: r.name,
457+
publishedAt: r.published_at
458+
}))
459+
460+
const currentVersion = opencodeServerManager.getVersion()
461+
462+
return c.json({
463+
versions,
464+
currentVersion
465+
})
466+
} catch (error) {
467+
logger.error('Failed to fetch OpenCode versions:', error)
468+
return c.json({
469+
error: 'Failed to fetch versions',
470+
details: error instanceof Error ? error.message : 'Unknown error'
471+
}, 500)
472+
}
473+
})
474+
475+
app.post('/opencode-install-version', async (c) => {
476+
try {
477+
const body = await c.req.json()
478+
const { version } = z.object({ version: z.string().min(1) }).parse(body)
479+
480+
logger.info(`Installing OpenCode version: ${version}`)
481+
482+
const oldVersion = opencodeServerManager.getVersion()
483+
logger.info(`Current OpenCode version: ${oldVersion}`)
484+
485+
const versionArg = version.startsWith('v') ? version : `v${version}`
486+
logger.info(`Running opencode upgrade ${versionArg}...`)
487+
488+
const upgradeOutput = execSync(`opencode upgrade ${versionArg} 2>&1`, { encoding: 'utf8' })
489+
logger.info(`Upgrade output: ${upgradeOutput}`)
490+
491+
await new Promise(r => setTimeout(r, 2000))
492+
493+
const newVersion = await opencodeServerManager.fetchVersion()
494+
logger.info(`New OpenCode version: ${newVersion}`)
495+
496+
opencodeServerManager.clearStartupError()
497+
await opencodeServerManager.restart()
498+
logger.info('OpenCode server restarted after version change')
499+
500+
return c.json({
501+
success: true,
502+
message: `OpenCode ${oldVersion ? `changed from v${oldVersion} to` : 'installed as'} v${newVersion}`,
503+
oldVersion,
504+
newVersion
505+
})
506+
} catch (error) {
507+
logger.error('Failed to install OpenCode version:', error)
508+
return c.json({
509+
error: 'Failed to install OpenCode version',
510+
details: error instanceof Error ? error.message : 'Unknown error'
511+
}, 500)
512+
}
513+
})
514+
418515
// Custom Commands routes
419516
app.get('/custom-commands', async (c) => {
420517
try {
@@ -540,8 +637,8 @@ export function createSettingsRoutes(db: Database) {
540637
await writeFileContent(agentsMdPath, content)
541638
logger.info(`Updated AGENTS.md at: ${agentsMdPath}`)
542639

543-
await opencodeServerManager.reloadConfig()
544-
logger.info('Reloaded OpenCode configuration after AGENTS.md update')
640+
await opencodeServerManager.restart()
641+
logger.info('Restarted OpenCode server after AGENTS.md update')
545642

546643
return c.json({ success: true })
547644
} catch (error) {

frontend/src/api/settings.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,29 @@ export const settingsApi = {
121121
return data
122122
},
123123

124+
getOpenCodeVersions: async (): Promise<{
125+
versions: Array<{
126+
version: string
127+
tag: string
128+
name: string
129+
publishedAt: string
130+
}>
131+
currentVersion: string | null
132+
}> => {
133+
const { data } = await axios.get(`${API_BASE_URL}/api/settings/opencode-versions`)
134+
return data
135+
},
136+
137+
installOpenCodeVersion: async (version: string): Promise<{
138+
success: boolean
139+
message: string
140+
oldVersion?: string
141+
newVersion?: string
142+
}> => {
143+
const { data } = await axios.post(`${API_BASE_URL}/api/settings/opencode-install-version`, { version })
144+
return data
145+
},
146+
124147
upgradeOpenCode: async (): Promise<{ success: boolean; message: string; oldVersion?: string; newVersion?: string; upgraded: boolean }> => {
125148
const { data } = await axios.post(`${API_BASE_URL}/api/settings/opencode-upgrade`)
126149
return data
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useMemo } from 'react'
2+
import { Check } from 'lucide-react'
3+
4+
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
5+
import {
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuTrigger,
10+
} from '@/components/ui/dropdown-menu'
11+
import { useAgents } from '@/hooks/useOpenCode'
12+
13+
interface AgentQuickSelectProps {
14+
opcodeUrl: string | null | undefined
15+
directory?: string
16+
currentAgent: string
17+
onAgentChange: (agent: string) => void
18+
isBashMode?: boolean
19+
disabled?: boolean
20+
}
21+
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+
}
43+
}
44+
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',
49+
}
50+
51+
export function AgentQuickSelect({
52+
opcodeUrl,
53+
directory,
54+
currentAgent,
55+
onAgentChange,
56+
isBashMode = false,
57+
disabled = false,
58+
}: AgentQuickSelectProps) {
59+
const { data: agents = [] } = useAgents(opcodeUrl, directory)
60+
61+
const primaryAgents = useMemo(() => {
62+
return agents.filter(
63+
(agent) =>
64+
(agent.mode === 'primary' || agent.mode === 'all') &&
65+
!agent.hidden
66+
)
67+
}, [agents])
68+
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+
79+
const handleSelect = (agentName: string) => {
80+
onAgentChange(agentName)
81+
}
82+
83+
const styles = isBashMode ? bashStyles : getAgentStyles(currentAgent)
84+
const displayName = isBashMode ? 'Bash' : capitalize(currentAgent)
85+
86+
const buttonContent = (
87+
<button
88+
data-toggle-mode
89+
onClick={primaryAgents.length <= 2 ? handleToggle : undefined}
90+
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}`}
92+
>
93+
<span className="truncate">{displayName}</span>
94+
</button>
95+
)
96+
97+
if (primaryAgents.length <= 2) {
98+
return buttonContent
99+
}
100+
101+
return (
102+
<DropdownMenu>
103+
<DropdownMenuTrigger asChild disabled={disabled}>
104+
{buttonContent}
105+
</DropdownMenuTrigger>
106+
<DropdownMenuContent align="start" className="w-48">
107+
{primaryAgents.map((agent) => {
108+
const agentStyles = getAgentStyles(agent.name)
109+
const isSelected = agent.name.toLowerCase() === currentAgent.toLowerCase()
110+
111+
return (
112+
<DropdownMenuItem
113+
key={agent.name}
114+
onClick={() => handleSelect(agent.name)}
115+
className="flex items-center justify-between"
116+
>
117+
<div className="flex flex-col">
118+
<span className={`font-medium ${agentStyles.color}`}>
119+
{capitalize(agent.name)}
120+
</span>
121+
{agent.description && (
122+
<span className="text-xs text-muted-foreground truncate max-w-[160px]">
123+
{agent.description}
124+
</span>
125+
)}
126+
</div>
127+
{isSelected && <Check className="h-4 w-4 flex-shrink-0 ml-2" />}
128+
</DropdownMenuItem>
129+
)
130+
})}
131+
</DropdownMenuContent>
132+
</DropdownMenu>
133+
)
134+
}

frontend/src/components/message/PromptInput.tsx

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SquareFill } from '@/components/ui/square-fill'
1818
import { MentionSuggestions, type MentionItem } from './MentionSuggestions'
1919
import { SessionStatusIndicator } from '@/components/ui/session-status-indicator'
2020
import { ModelQuickSelect } from '@/components/model/ModelQuickSelect'
21+
import { AgentQuickSelect } from '@/components/agent/AgentQuickSelect'
2122
import { detectMentionTrigger, parsePromptToParts, getFilename, filterAgentsByQuery } from '@/lib/promptParser'
2223

2324

@@ -346,9 +347,8 @@ export const PromptInput = forwardRef<PromptInputHandle, PromptInputProps>(funct
346347
setMentionRange(null)
347348
}
348349

349-
const handleModeToggle = () => {
350-
const newMode = currentMode === 'plan' ? 'build' : 'plan'
351-
setLocalMode(newMode)
350+
const handleAgentChange = (agent: string) => {
351+
setLocalMode(agent)
352352
}
353353

354354
const addImageAttachment = (file: File) => {
@@ -682,9 +682,6 @@ if (isIOS && isSecureContext && navigator.clipboard && navigator.clipboard.read)
682682

683683
const sessionAgent = useSessionAgent(opcodeUrl, sessionID, directory)
684684
const currentMode = localMode ?? sessionAgent.agent
685-
const modeColor = currentMode === 'plan' ? 'text-yellow-600 dark:text-yellow-500' : 'text-green-600 dark:text-green-500'
686-
const modeBg = currentMode === 'plan' ? 'bg-yellow-500/20 border-yellow-400 hover:bg-yellow-500/30 hover:border-yellow-300' : 'bg-green-500/20 border-green-400 hover:bg-green-500/30 hover:border-green-300'
687-
const modeShadow = currentMode === 'plan' ? 'shadow-yellow-500/20 hover:shadow-yellow-500/30' : 'shadow-green-500/20 hover:shadow-green-500/30'
688685

689686
const { model, modelString } = useModelSelection(opcodeUrl, directory)
690687
const currentModel = modelString || ''
@@ -777,17 +774,14 @@ return (
777774

778775
<div className="flex gap-1.5 md:gap-2 items-center justify-between">
779776
<div className="flex gap-1.5 md:gap-2 items-center">
780-
<button
781-
data-toggle-mode
782-
onClick={handleModeToggle}
783-
className={`px-3 md:px-3.5 py-1 h-[36px] rounded-lg text-sm font-medium border w-14 flex items-center justify-center transition-all duration-200 active:scale-95 hover:scale-105 shadow-md ${
784-
isBashMode
785-
? 'bg-purple-500/20 border-purple-400 text-purple-700 dark:text-purple-300 shadow-purple-500/20 hover:shadow-purple-500/30'
786-
: `${modeBg} ${modeColor} ${modeShadow}`
787-
}`}
788-
>
789-
{isBashMode ? 'BASH' : currentMode.toUpperCase()}
790-
</button>
777+
<AgentQuickSelect
778+
opcodeUrl={opcodeUrl}
779+
directory={directory}
780+
currentAgent={currentMode}
781+
onAgentChange={handleAgentChange}
782+
isBashMode={isBashMode}
783+
disabled={disabled}
784+
/>
791785
{hasActiveStream ? (
792786
<div className="px-2.5 py-1.5 md:px-3 md:py-2 rounded-lg text-xs md:text-sm font-medium text-muted-foreground max-w-[120px] md:max-w-[180px]">
793787
<SessionStatusIndicator sessionID={sessionID} />

0 commit comments

Comments
 (0)