Compare commits

..

16 Commits

Author SHA1 Message Date
opencode e9de7f95a7 release: v0.4.18 2025-08-11 16:04:07 +00:00
adamdotdevin a4113acd15 fix: assistant message footer styles 2025-08-11 10:57:18 -05:00
adamdotdevin 9c8e56fc96 fix: assistant message footer styles 2025-08-11 10:52:49 -05:00
adamdotdevin c78cb57c41 fix: assistant message footer styles 2025-08-11 10:50:00 -05:00
opencode eb15b2ba75 release: v0.4.17 2025-08-11 15:15:24 +00:00
Dax Raad 279edb6f24 fix azure gpt config 2025-08-11 10:56:16 -04:00
Dax Raad c51a34bf4b make models key optional in config 2025-08-11 10:54:14 -04:00
adamdotdevin e8d144d2a2 fix: reformat assistant message footer 2025-08-11 09:38:52 -05:00
adamdotdevin a760e8364f feat: placeholder on pending assistant message 2025-08-11 09:29:44 -05:00
adamdotdevin fa7cae59c0 fix: re-render messages on session error 2025-08-11 09:19:45 -05:00
spoons-and-mirrors 8780fa6ccf Fix: Respect agent's preferred model at TUI startup (#1683)
Co-authored-by: opencode <noreply@opencode.ai>
2025-08-11 08:51:35 -05:00
spoons-and-mirrors ab2df0ae33 Feat: Implement Wrap-Around Navigation for List Selection (for Models and Tools modal) (#1768) 2025-08-11 08:47:51 -05:00
Timo Clasen 23757f3ac0 fix: only load the first local and global rule file (#1761) 2025-08-11 08:28:03 -05:00
Aiden Cline df7296cfe1 fix: instructions should be able to handle absolute paths (#1762) 2025-08-11 08:23:41 -05:00
opencode 776276d5a4 release: v0.4.16 2025-08-11 12:59:20 +00:00
Dax Raad eea45a22fa ci: tweak 2025-08-11 08:54:14 -04:00
19 changed files with 190 additions and 58 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.4.15",
"version": "0.4.18",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-function",
"version": "0.4.15",
"version": "0.4.18",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-web",
"version": "0.4.15",
"version": "0.4.18",
"private": true,
"description": "",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.4.15",
"version": "0.4.18",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.4.15",
"version": "0.4.18",
"name": "opencode",
"type": "module",
"private": true,
+1 -1
View File
@@ -294,7 +294,7 @@ export namespace Config {
.record(
ModelsDev.Provider.partial()
.extend({
models: z.record(ModelsDev.Model.partial()),
models: z.record(ModelsDev.Model.partial()).optional(),
options: z
.object({
apiKey: z.string().optional(),
+7 -1
View File
@@ -82,8 +82,14 @@ export namespace ProviderTransform {
return undefined
}
export function options(_providerID: string, modelID: string) {
export function options(providerID: string, modelID: string): Record<string, any> | undefined {
if (modelID.includes("gpt-5")) {
if (providerID === "azure") {
return {
reasoning_effort: "minimal",
text_verbosity: "verbose",
}
}
return {
reasoningEffort: "minimal",
textVerbosity: "low",
+33 -8
View File
@@ -54,28 +54,53 @@ export namespace SystemPrompt {
]
}
const CUSTOM_FILES = [
const LOCAL_RULE_FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
const GLOBAL_RULE_FILES = [
path.join(Global.Path.config, "AGENTS.md"),
path.join(os.homedir(), ".claude", "CLAUDE.md"),
]
export async function custom() {
const { cwd, root } = App.info().path
const config = await Config.get()
const paths = new Set<string>()
for (const item of CUSTOM_FILES) {
const matches = await Filesystem.findUp(item, cwd, root)
matches.forEach((path) => paths.add(path))
for (const localRuleFile of LOCAL_RULE_FILES) {
const matches = await Filesystem.findUp(localRuleFile, cwd, root)
if (matches.length > 0) {
matches.forEach((path) => paths.add(path))
break
}
}
paths.add(path.join(Global.Path.config, "AGENTS.md"))
paths.add(path.join(os.homedir(), ".claude", "CLAUDE.md"))
for (const globalRuleFile of GLOBAL_RULE_FILES) {
if (await Bun.file(globalRuleFile).exists()) {
paths.add(globalRuleFile)
break
}
}
if (config.instructions) {
for (const instruction of config.instructions) {
const matches = await Filesystem.globUp(instruction, cwd, root).catch(() => [])
for (let instruction of config.instructions) {
if (instruction.startsWith("~/")) {
instruction = path.join(os.homedir(), instruction.slice(2))
}
let matches: string[] = []
if (path.isAbsolute(instruction)) {
matches = await Array.fromAsync(
new Bun.Glob(path.basename(instruction)).scan({
cwd: path.dirname(instruction),
absolute: true,
onlyFiles: true,
}),
).catch(() => [])
} else {
matches = await Filesystem.globUp(instruction, cwd, root).catch(() => [])
}
matches.forEach((path) => paths.add(path))
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.4.15",
"version": "0.4.18",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.4.15",
"version": "0.4.18",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+1 -1
View File
@@ -627,7 +627,7 @@ export type Config = {
env?: Array<string>
id?: string
npm?: string
models: {
models?: {
[key: string]: {
id?: string
name?: string
+14 -3
View File
@@ -417,7 +417,18 @@ func (a *App) InitializeProvider() tea.Cmd {
}
}
// Priority 3: Recent model usage (most recently used model)
// Priority 3: Current agent's preferred model
if selectedProvider == nil && a.Agent().Model.ModelID != "" {
if provider, model := findModelByProviderAndModelID(providers, a.Agent().Model.ProviderID, a.Agent().Model.ModelID); provider != nil && model != nil {
selectedProvider = provider
selectedModel = model
slog.Debug("Selected model from current agent", "provider", provider.ID, "model", model.ID, "agent", a.Agent().Name)
} else {
slog.Debug("Agent model not found", "provider", a.Agent().Model.ProviderID, "model", a.Agent().Model.ModelID, "agent", a.Agent().Name)
}
}
// Priority 4: Recent model usage (most recently used model)
if selectedProvider == nil && len(a.State.RecentlyUsedModels) > 0 {
recentUsage := a.State.RecentlyUsedModels[0] // Most recent is first
if provider, model := findModelByProviderAndModelID(providers, recentUsage.ProviderID, recentUsage.ModelID); provider != nil &&
@@ -436,7 +447,7 @@ func (a *App) InitializeProvider() tea.Cmd {
}
}
// Priority 4: State-based model (backwards compatibility)
// Priority 5: State-based model (backwards compatibility)
if selectedProvider == nil && a.State.Provider != "" && a.State.Model != "" {
if provider, model := findModelByProviderAndModelID(providers, a.State.Provider, a.State.Model); provider != nil &&
model != nil {
@@ -448,7 +459,7 @@ func (a *App) InitializeProvider() tea.Cmd {
}
}
// Priority 5: Internal priority fallback (Anthropic preferred, then first available)
// Priority 6: Internal priority fallback (Anthropic preferred, then first available)
if selectedProvider == nil {
// Try Anthropic first as internal priority
if provider := findProviderByID(providers, "anthropic"); provider != nil {
@@ -220,14 +220,17 @@ func renderText(
var content string
switch casted := message.(type) {
case opencode.AssistantMessage:
bg := t.Background()
backgroundColor = t.Background()
if isThinking {
bg = t.BackgroundPanel()
backgroundColor = t.BackgroundPanel()
}
ts = time.UnixMilli(int64(casted.Time.Created))
content = util.ToMarkdown(text, width, bg)
if casted.Time.Completed > 0 {
ts = time.UnixMilli(int64(casted.Time.Completed))
}
content = util.ToMarkdown(text, width, backgroundColor)
if isThinking {
content = styles.NewStyle().Background(bg).Foreground(t.TextMuted()).Render("Thinking") + "\n\n" + content
content = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking") + "\n\n" + content
}
case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created))
@@ -332,8 +335,12 @@ func renderText(
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
timestamp = timestamp[12:]
}
timestamp = styles.NewStyle().
Background(backgroundColor).
Foreground(t.TextMuted()).
Render(" (" + timestamp + ")")
// Check if this is an assistant message with mode (agent) information
// Check if this is an assistant message with agent information
var modelAndAgentSuffix string
if assistantMsg, ok := message.(opencode.AssistantMessage); ok && assistantMsg.Mode != "" {
// Find the agent index by name to get the correct color
@@ -349,22 +356,26 @@ func renderText(
agentColor := util.GetAgentColor(agentIndex)
// Style the agent name with the same color as status bar
agentName := strings.Title(assistantMsg.Mode)
styledAgentName := styles.NewStyle().Foreground(agentColor).Bold(true).Render(agentName)
modelAndAgentSuffix = fmt.Sprintf(" | %s | %s", assistantMsg.ModelID, styledAgentName)
agentName := cases.Title(language.Und).String(assistantMsg.Mode)
styledAgentName := styles.NewStyle().
Background(backgroundColor).
Foreground(agentColor).
Render(agentName + " ")
styledModelID := styles.NewStyle().
Background(backgroundColor).
Foreground(t.TextMuted()).
Render(assistantMsg.ModelID)
modelAndAgentSuffix = styledAgentName + styledModelID
}
var info string
if modelAndAgentSuffix != "" {
// For assistant messages: "timestamp | modelID | agentName"
info = fmt.Sprintf("%s%s", timestamp, modelAndAgentSuffix)
info = modelAndAgentSuffix + timestamp
} else {
// For user messages: "author (timestamp)"
info = fmt.Sprintf("%s (%s)", author, timestamp)
info = author + timestamp
}
info = styles.NewStyle().Foreground(t.TextMuted()).Render(info)
if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
content = content + "\n\n"
content = content + "\n"
for _, toolCall := range toolCalls {
title := renderToolTitle(toolCall, width-2)
style := styles.NewStyle()
@@ -372,15 +383,16 @@ func renderText(
style = style.Foreground(t.Error())
}
title = style.Render(title)
title = "∟ " + title + "\n"
title = "\n∟ " + title
content = content + title
}
}
sections := []string{content, info}
sections := []string{content}
if extra != "" {
sections = append(sections, "\n"+extra)
}
sections = append(sections, "\n"+info)
content = strings.Join(sections, "\n")
switch message.(type) {
@@ -187,6 +187,10 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.Properties.Info.SessionID == m.app.Session.ID {
cmds = append(cmds, m.renderView())
}
case opencode.EventListResponseEventSessionError:
if msg.Properties.SessionID == m.app.Session.ID {
cmds = append(cmds, m.renderView())
}
case opencode.EventListResponseEventMessagePartUpdated:
if msg.Properties.Part.SessionID == m.app.Session.ID {
cmds = append(cmds, m.renderView())
@@ -396,6 +400,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
revertedToolCount = 0
}
hasTextPart := false
hasContent := false
for partIndex, p := range message.Parts {
switch part := p.(type) {
case opencode.TextPart:
@@ -487,6 +492,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
hasContent = true
}
case opencode.ToolPart:
if reverted {
@@ -548,6 +554,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
hasContent = true
}
case opencode.ReasoningPart:
if reverted {
@@ -578,8 +585,33 @@ func (m *messagesComponent) renderView() tea.Cmd {
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
hasContent = true
}
}
if !hasContent {
content = renderText(
m.app,
message.Info,
"Generating...",
casted.ModelID,
m.showToolDetails,
width,
"",
false,
[]opencode.FilePart{},
[]opencode.AgentPart{},
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
partCount++
lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
error := ""
+17 -12
View File
@@ -173,7 +173,13 @@ func (c *listComponent[T]) moveUp() {
}
}
// If no selectable item found above, stay at current position
// If no selectable item found above, wrap to the bottom
for i := len(c.items) - 1; i > c.selectedIdx; i-- {
if c.isSelectable(c.items[i]) {
c.selectedIdx = i
return
}
}
}
// moveDown moves the selection down, skipping non-selectable items
@@ -183,20 +189,19 @@ func (c *listComponent[T]) moveDown() {
}
originalIdx := c.selectedIdx
for {
if c.selectedIdx < len(c.items)-1 {
c.selectedIdx++
} else {
break
}
if c.isSelectable(c.items[c.selectedIdx]) {
// First try moving down from current position
for i := c.selectedIdx + 1; i < len(c.items); i++ {
if c.isSelectable(c.items[i]) {
c.selectedIdx = i
return
}
}
// Prevent infinite loop
if c.selectedIdx == originalIdx {
break
// If no selectable item found below, wrap to the top
for i := 0; i < originalIdx; i++ {
if c.isSelectable(c.items[i]) {
c.selectedIdx = i
return
}
}
}
@@ -138,15 +138,18 @@ func TestCtrlNavigation(t *testing.T) {
func TestNavigationBoundaries(t *testing.T) {
list := createTestList()
// Test up arrow at first item (should stay at 0)
// Test up arrow at first item (should wrap to last item)
upKey := tea.KeyPressMsg{Code: tea.KeyUp}
updatedModel, _ := list.Update(upKey)
list = updatedModel.(*listComponent[testItem])
_, idx := list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected to stay at index 0 when pressing up at first item, got %d", idx)
if idx != 2 {
t.Errorf("Expected to wrap to index 2 when pressing up at first item, got %d", idx)
}
// Move to first item
list.SetSelectedIndex(0)
// Move to last item
downKey := tea.KeyPressMsg{Code: tea.KeyDown}
updatedModel, _ = list.Update(downKey)
@@ -158,12 +161,12 @@ func TestNavigationBoundaries(t *testing.T) {
t.Errorf("Expected to be at index 2, got %d", idx)
}
// Test down arrow at last item (should stay at 2)
// Test down arrow at last item (should wrap to first item)
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 2 {
t.Errorf("Expected to stay at index 2 when pressing down at last item, got %d", idx)
if idx != 0 {
t.Errorf("Expected to wrap to index 0 when pressing down at last item, got %d", idx)
}
}
@@ -208,3 +211,39 @@ func TestEmptyList(t *testing.T) {
t.Error("Expected IsEmpty() to return true for empty list")
}
}
func TestWrapAroundNavigation(t *testing.T) {
list := createTestList()
// Start at first item (index 0)
_, idx := list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected to start at index 0, got %d", idx)
}
// Press up arrow - should wrap to last item (index 2)
upKey := tea.KeyPressMsg{Code: tea.KeyUp}
updatedModel, _ := list.Update(upKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 2 {
t.Errorf("Expected to wrap to index 2 when pressing up from first item, got %d", idx)
}
// Press down arrow - should wrap to first item (index 0)
downKey := tea.KeyPressMsg{Code: tea.KeyDown}
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 0 {
t.Errorf("Expected to wrap to index 0 when pressing down from last item, got %d", idx)
}
// Navigate to middle and verify normal navigation still works
updatedModel, _ = list.Update(downKey)
list = updatedModel.(*listComponent[testItem])
_, idx = list.GetSelectedItem()
if idx != 1 {
t.Errorf("Expected to move to index 1, got %d", idx)
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.4.15",
"version": "0.4.18",
"scripts": {
"dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
+3 -1
View File
@@ -45,7 +45,9 @@ process.chdir(dir)
if (!snapshot) {
await $`git commit -am "release: v${version}"`
await $`git tag v${version}`
await $`git push origin HEAD --tags --no-verify`
await $`git fetch origin`
await $`git cherry-pick HEAD..origin/dev`.nothrow()
await $`git push origin HEAD --tags --no-verify --force`
const previous = await fetch("https://api.github.com/repos/sst/opencode/releases/latest")
.then((res) => {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.4.15",
"version": "0.4.18",
"publisher": "sst-dev",
"repository": {
"type": "git",