Skip to content

Commit de8fd68

Browse files
committed
feat: support opencode
1 parent 2c4fccb commit de8fd68

File tree

8 files changed

+70
-7
lines changed

8 files changed

+70
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ agentapi server -- goose
6565
```
6666

6767
> [!NOTE]
68-
> When using Codex, Gemini, Amp or CursorCLI, always specify the agent type explicitly (eg: `agentapi server --type=codex -- codex`), or message formatting may break.
68+
> When using Codex, Opencode, Gemini, Amp or CursorCLI, always specify the agent type explicitly (eg: `agentapi server --type=codex -- codex`), or message formatting may break.
6969
7070
An OpenAPI schema is available in [openapi.json](openapi.json).
7171

cmd/server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
AgentTypeCursorAgent AgentType = msgfmt.AgentTypeCursorAgent
3333
AgentTypeCursor AgentType = msgfmt.AgentTypeCursor
3434
AgentTypeAuggie AgentType = msgfmt.AgentTypeAuggie
35+
AgentTypeOpencode AgentType = msgfmt.AgentTypeOpencode
3536
AgentTypeCustom AgentType = msgfmt.AgentTypeCustom
3637
)
3738

@@ -46,6 +47,7 @@ var agentTypeMap = map[AgentType]bool{
4647
AgentTypeCursorAgent: true,
4748
AgentTypeCursor: true,
4849
AgentTypeAuggie: true,
50+
AgentTypeOpencode: true,
4951
AgentTypeCustom: true,
5052
}
5153

cmd/server/server_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ func TestParseAgentType(t *testing.T) {
5757
agentTypeVar: "",
5858
want: AgentTypeCursor,
5959
},
60+
{
61+
firstArg: "opencode",
62+
agentTypeVar: "",
63+
want: AgentTypeOpencode,
64+
},
6065
{
6166
firstArg: "auggie",
6267
agentTypeVar: "",
@@ -97,6 +102,11 @@ func TestParseAgentType(t *testing.T) {
97102
agentTypeVar: "gemini",
98103
want: AgentTypeGemini,
99104
},
105+
{
106+
firstArg: "claude",
107+
agentTypeVar: "opencode",
108+
want: AgentTypeOpencode,
109+
},
100110
{
101111
firstArg: "claude",
102112
agentTypeVar: "cursor-agent",

lib/httpapi/server.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
196196
return mf.FormatAgentMessage(config.AgentType, message, userInput)
197197
}
198198
conversation := st.NewConversation(ctx, st.ConversationConfig{
199-
AgentIO: config.Process,
199+
AgentType: config.AgentType,
200+
AgentIO: config.Process,
200201
GetTime: func() time.Time {
201202
return time.Now()
202203
},

lib/msgfmt/message_box.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,21 @@ func removeCodexInputBox(msg string) string {
6060
}
6161
return strings.Join(lines, "\n")
6262
}
63+
64+
func removeOpencodeMessageBox(msg string) string {
65+
lines := strings.Split(msg, "\n")
66+
// Check the last 3 lines for
67+
//
68+
// ┃ ┃
69+
// ┃ > ┃
70+
// ┃ ┃
71+
// We only check for the first ┃ and then an empty line above it as sometimes the full block load within a snapshot this leads to ..
72+
for i := len(lines) - 1; i >= 1; i-- {
73+
if strings.TrimSpace(lines[i-1]) == "" &&
74+
strings.ReplaceAll(lines[i], " ", "") == "┃┃" {
75+
lines = lines[:i-1]
76+
break
77+
}
78+
}
79+
return strings.Join(lines, "\n")
80+
}

lib/msgfmt/msgfmt.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,13 @@ func RemoveUserInput(msgRaw string, userInputRaw string, agentType AgentType) st
196196
if idx, found := skipTrailingInputBoxLine(msgLines, lastUserInputLineIdx, "┘", "└"); found {
197197
lastUserInputLineIdx = idx
198198
}
199+
} else if agentType == AgentTypeOpencode {
200+
// skip +2 lines after the input
201+
// ┃ jkmr (08:46 PM) ┃
202+
// ┃ ┃
203+
if lastUserInputLineIdx+2 < len(msgLines) {
204+
lastUserInputLineIdx += 2
205+
}
199206
}
200207

201208
return strings.Join(msgLines[lastUserInputLineIdx+1:], "\n")
@@ -234,6 +241,7 @@ const (
234241
AgentTypeCursorAgent AgentType = "cursor-agent"
235242
AgentTypeCursor AgentType = "cursor"
236243
AgentTypeAuggie AgentType = "auggie"
244+
AgentTypeOpencode AgentType = "opencode"
237245
AgentTypeCustom AgentType = "custom"
238246
)
239247

@@ -251,6 +259,13 @@ func formatCodexMessage(message string, userInput string) string {
251259
return message
252260
}
253261

262+
func formatOpencodeMessage(message string, userInput string) string {
263+
message = RemoveUserInput(message, userInput, AgentTypeOpencode)
264+
message = removeOpencodeMessageBox(message)
265+
message = trimEmptyLines(message)
266+
return message
267+
}
268+
254269
func FormatAgentMessage(agentType AgentType, message string, userInput string) string {
255270
switch agentType {
256271
case AgentTypeClaude:
@@ -271,6 +286,8 @@ func FormatAgentMessage(agentType AgentType, message string, userInput string) s
271286
return formatGenericMessage(message, userInput, agentType)
272287
case AgentTypeAuggie:
273288
return formatGenericMessage(message, userInput, agentType)
289+
case AgentTypeOpencode:
290+
return formatOpencodeMessage(message, userInput)
274291
case AgentTypeCustom:
275292
return formatGenericMessage(message, userInput, agentType)
276293
default:

lib/msgfmt/msgfmt_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func TestTrimEmptyLines(t *testing.T) {
218218

219219
func TestFormatAgentMessage(t *testing.T) {
220220
dir := "testdata/format"
221-
agentTypes := []AgentType{AgentTypeClaude, AgentTypeGoose, AgentTypeAider, AgentTypeGemini, AgentTypeAmp, AgentTypeCodex, AgentTypeCursorAgent, AgentTypeCursor, AgentTypeAuggie, AgentTypeCustom}
221+
agentTypes := []AgentType{AgentTypeClaude, AgentTypeGoose, AgentTypeAider, AgentTypeGemini, AgentTypeAmp, AgentTypeCodex, AgentTypeCursorAgent, AgentTypeCursor, AgentTypeAuggie, AgentTypeOpencode, AgentTypeCustom}
222222
for _, agentType := range agentTypes {
223223
t.Run(string(agentType), func(t *testing.T) {
224224
cases, err := testdataDir.ReadDir(path.Join(dir, string(agentType)))

lib/screentracker/conversation.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ type AgentIO interface {
2424
}
2525

2626
type ConversationConfig struct {
27-
AgentIO AgentIO
27+
AgentType msgfmt.AgentType
28+
AgentIO AgentIO
2829
// GetTime returns the current time
2930
GetTime func() time.Time
3031
// How often to take a snapshot for the stability check
@@ -133,15 +134,29 @@ func (c *Conversation) StartSnapshotLoop(ctx context.Context) {
133134
}()
134135
}
135136

136-
func FindNewMessage(oldScreen, newScreen string) string {
137+
func FindNewMessage(oldScreen, newScreen string, agentType msgfmt.AgentType) string {
137138
oldLines := strings.Split(oldScreen, "\n")
138139
newLines := strings.Split(newScreen, "\n")
139140
oldLinesMap := make(map[string]bool)
141+
142+
// -1 indicates no header
143+
dynamicHeaderEnd := -1
144+
145+
// Skip header lines for Opencode agent type to avoid false positives
146+
// The header contains dynamic content (token count, context percentage, cost)
147+
// that changes between screens, causing line comparison mismatches:
148+
//
149+
// ┃ # Getting Started with Claude CLI ┃
150+
// ┃ /share to create a shareable link 12.6K/6% ($0.05) ┃
151+
if len(newLines) >= 2 && agentType == msgfmt.AgentTypeOpencode {
152+
dynamicHeaderEnd = 2
153+
}
154+
140155
for _, line := range oldLines {
141156
oldLinesMap[line] = true
142157
}
143158
firstNonMatchingLine := len(newLines)
144-
for i, line := range newLines {
159+
for i, line := range newLines[dynamicHeaderEnd+1:] {
145160
if !oldLinesMap[line] {
146161
firstNonMatchingLine = i
147162
break
@@ -178,7 +193,7 @@ func (c *Conversation) lastMessage(role ConversationRole) ConversationMessage {
178193

179194
// This function assumes that the caller holds the lock
180195
func (c *Conversation) updateLastAgentMessage(screen string, timestamp time.Time) {
181-
agentMessage := FindNewMessage(c.screenBeforeLastUserMessage, screen)
196+
agentMessage := FindNewMessage(c.screenBeforeLastUserMessage, screen, c.cfg.AgentType)
182197
lastUserMessage := c.lastMessage(ConversationRoleUser)
183198
if c.cfg.FormatMessage != nil {
184199
agentMessage = c.cfg.FormatMessage(agentMessage, lastUserMessage.Message)

0 commit comments

Comments
 (0)