Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,108 changes: 2,108 additions & 0 deletions docs/opencode-server-endpoints.md

Large diffs are not rendered by default.

1,089 changes: 1,089 additions & 0 deletions docs/tui-event-specification.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions go-opencode/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.2 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go-opencode/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMi
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
Expand Down
17 changes: 17 additions & 0 deletions go-opencode/internal/event/bus.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ const (
PermissionReplied EventType = "permission.replied" // SDK compatible (was permission.resolved)
TodoUpdated EventType = "todo.updated"

// TUI Events
TuiPromptAppend EventType = "tui.prompt.append"
TuiCommandExecute EventType = "tui.command.execute"
TuiToastShow EventType = "tui.toast.show"

// VCS Events
VcsBranchUpdated EventType = "vcs.branch.updated"

// PTY Events
PtyCreated EventType = "pty.created"
PtyUpdated EventType = "pty.updated"
PtyExited EventType = "pty.exited"
PtyDeleted EventType = "pty.deleted"

// Command Events
CommandExecuted EventType = "command.executed"

// Client Tool Events
ClientToolRequest EventType = "client-tool.request"
ClientToolRegistered EventType = "client-tool.registered"
Expand Down
71 changes: 71 additions & 0 deletions go-opencode/internal/event/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,74 @@ type ClientToolStatusData struct {
Error string `json:"error,omitempty"`
Success bool `json:"success,omitempty"`
}

// TUI Events

// TuiPromptAppendData is the data for tui.prompt.append events.
type TuiPromptAppendData struct {
Text string `json:"text"`
}

// TuiCommandExecuteData is the data for tui.command.execute events.
type TuiCommandExecuteData struct {
Command string `json:"command"`
}

// TuiToastShowData is the data for tui.toast.show events.
type TuiToastShowData struct {
Title string `json:"title,omitempty"`
Message string `json:"message"`
Variant string `json:"variant"` // "info" | "success" | "warning" | "error"
Duration int `json:"duration,omitempty"`
}

// VCS Events

// VcsBranchUpdatedData is the data for vcs.branch.updated events.
type VcsBranchUpdatedData struct {
Branch string `json:"branch,omitempty"`
}

// PTY Events

// PtyInfo represents a PTY session.
type PtyInfo struct {
ID string `json:"id"`
Title string `json:"title"`
Command string `json:"command"`
Args []string `json:"args"`
Cwd string `json:"cwd"`
Status string `json:"status"` // "running" | "exited"
Pid int `json:"pid"`
}

// PtyCreatedData is the data for pty.created events.
type PtyCreatedData struct {
Info PtyInfo `json:"info"`
}

// PtyUpdatedData is the data for pty.updated events.
type PtyUpdatedData struct {
Info PtyInfo `json:"info"`
}

// PtyExitedData is the data for pty.exited events.
type PtyExitedData struct {
ID string `json:"id"`
ExitCode int `json:"exitCode"`
}

// PtyDeletedData is the data for pty.deleted events.
type PtyDeletedData struct {
ID string `json:"id"`
}

// Command Events

// CommandExecutedData is the data for command.executed events.
type CommandExecutedData struct {
Name string `json:"name"`
SessionID string `json:"sessionID"`
Arguments string `json:"arguments"`
MessageID string `json:"messageID"`
}
40 changes: 30 additions & 10 deletions go-opencode/internal/server/handlers_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/go-chi/chi/v5"
"github.com/opencode-ai/opencode/internal/event"

"github.com/opencode-ai/opencode/internal/command"
"github.com/opencode-ai/opencode/internal/mcp"
Expand Down Expand Up @@ -74,12 +75,18 @@ type ModalityCapabilities struct {
PDF bool `json:"pdf"`
}

// ModelCostCache represents cache pricing (nested structure for TS compatibility).
type ModelCostCache struct {
Read float64 `json:"read"`
Write float64 `json:"write"`
}

// ModelCost represents model pricing.
// SDK compatible: uses nested cache object to match TypeScript structure.
type ModelCost struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
CacheRead float64 `json:"cache_read,omitempty"`
CacheWrite float64 `json:"cache_write,omitempty"`
Input float64 `json:"input"`
Output float64 `json:"output"`
Cache ModelCostCache `json:"cache"` // Nested object, not flat fields
}

// ModelLimit represents model limits.
Expand Down Expand Up @@ -126,7 +133,7 @@ func getDefaultProviders() []ProviderInfo {
Input: ModalityCapabilities{Text: true, Audio: false, Image: true, Video: false, PDF: true},
Output: ModalityCapabilities{Text: true, Audio: false, Image: false, Video: false, PDF: false},
},
Cost: ModelCost{Input: 3.0, Output: 15.0, CacheRead: 0.3, CacheWrite: 3.75},
Cost: ModelCost{Input: 3.0, Output: 15.0, Cache: ModelCostCache{Read: 0.3, Write: 3.75}},
Limit: ModelLimit{Context: 200000, Output: 64000},
Options: map[string]any{},
},
Expand All @@ -142,7 +149,7 @@ func getDefaultProviders() []ProviderInfo {
Input: ModalityCapabilities{Text: true, Audio: false, Image: true, Video: false, PDF: true},
Output: ModalityCapabilities{Text: true, Audio: false, Image: false, Video: false, PDF: false},
},
Cost: ModelCost{Input: 15.0, Output: 75.0, CacheRead: 1.5, CacheWrite: 18.75},
Cost: ModelCost{Input: 15.0, Output: 75.0, Cache: ModelCostCache{Read: 1.5, Write: 18.75}},
Limit: ModelLimit{Context: 200000, Output: 32000},
Options: map[string]any{},
},
Expand All @@ -158,7 +165,7 @@ func getDefaultProviders() []ProviderInfo {
Input: ModalityCapabilities{Text: true, Audio: false, Image: true, Video: false, PDF: true},
Output: ModalityCapabilities{Text: true, Audio: false, Image: false, Video: false, PDF: false},
},
Cost: ModelCost{Input: 0.8, Output: 4.0, CacheRead: 0.08, CacheWrite: 1.0},
Cost: ModelCost{Input: 0.8, Output: 4.0, Cache: ModelCostCache{Read: 0.08, Write: 1.0}},
Limit: ModelLimit{Context: 200000, Output: 8192},
Options: map[string]any{},
},
Expand All @@ -182,7 +189,7 @@ func getDefaultProviders() []ProviderInfo {
Input: ModalityCapabilities{Text: true, Audio: false, Image: true, Video: false, PDF: false},
Output: ModalityCapabilities{Text: true, Audio: false, Image: false, Video: false, PDF: false},
},
Cost: ModelCost{Input: 2.5, Output: 10.0},
Cost: ModelCost{Input: 2.5, Output: 10.0, Cache: ModelCostCache{Read: 0, Write: 0}},
Limit: ModelLimit{Context: 128000, Output: 16384},
Options: map[string]any{},
},
Expand All @@ -198,7 +205,7 @@ func getDefaultProviders() []ProviderInfo {
Input: ModalityCapabilities{Text: true, Audio: false, Image: true, Video: false, PDF: false},
Output: ModalityCapabilities{Text: true, Audio: false, Image: false, Video: false, PDF: false},
},
Cost: ModelCost{Input: 0.15, Output: 0.6},
Cost: ModelCost{Input: 0.15, Output: 0.6, Cache: ModelCostCache{Read: 0, Write: 0}},
Limit: ModelLimit{Context: 128000, Output: 16384},
Options: map[string]any{},
},
Expand Down Expand Up @@ -937,7 +944,9 @@ func (s *Server) executeCommand(w http.ResponseWriter, r *http.Request) {
}

var req struct {
Args string `json:"args"`
Args string `json:"args"`
SessionID string `json:"sessionID,omitempty"`
MessageID string `json:"messageID,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// Empty body is ok
Expand All @@ -950,6 +959,17 @@ func (s *Server) executeCommand(w http.ResponseWriter, r *http.Request) {
return
}

// Publish command.executed event
event.PublishSync(event.Event{
Type: event.CommandExecuted,
Data: event.CommandExecutedData{
Name: name,
SessionID: req.SessionID,
Arguments: req.Args,
MessageID: req.MessageID,
},
})

writeJSON(w, http.StatusOK, result)
}

Expand Down
15 changes: 10 additions & 5 deletions go-opencode/internal/server/handlers_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"

"github.com/opencode-ai/opencode/internal/lsp"
"github.com/opencode-ai/opencode/internal/vcs"
)

// FileInfo represents file information.
Expand Down Expand Up @@ -263,13 +264,17 @@ var symbolKindsFilter = map[lsp.SymbolKind]bool{
func (s *Server) getVCSInfo(w http.ResponseWriter, r *http.Request) {
directory := getDirectory(r.Context())

// Get current branch
cmd := exec.Command("git", "branch", "--show-current")
cmd.Dir = directory
branch, _ := cmd.Output()
// Use cached branch from watcher if available for the same directory
var branch string
if s.vcsWatcher != nil && directory == s.config.Directory {
branch = s.vcsWatcher.CurrentBranch()
} else {
// Fallback to direct git command
branch = vcs.GetBranch(directory)
}

writeJSON(w, http.StatusOK, map[string]any{
"branch": strings.TrimSpace(string(branch)),
"branch": branch,
})
}

Expand Down
71 changes: 65 additions & 6 deletions go-opencode/internal/server/handlers_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/opencode-ai/opencode/internal/clienttool"
"github.com/opencode-ai/opencode/internal/event"
)

// TUI control handlers for the TUI client.
Expand All @@ -19,7 +20,13 @@ func (s *Server) tuiAppendPrompt(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Invalid JSON body")
return
}
// TUI would receive this via SSE

// Publish event for TUI clients via SSE
event.PublishSync(event.Event{
Type: event.TuiPromptAppend,
Data: event.TuiPromptAppendData{Text: req.Text},
})

writeSuccess(w)
}

Expand All @@ -32,32 +39,84 @@ func (s *Server) tuiExecuteCommand(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Invalid JSON body")
return
}

// Publish event for TUI clients via SSE
event.PublishSync(event.Event{
Type: event.TuiCommandExecute,
Data: event.TuiCommandExecuteData{Command: req.Command},
})

writeSuccess(w)
}

// tuiShowToast handles POST /tui/show-toast
func (s *Server) tuiShowToast(w http.ResponseWriter, r *http.Request) {
var req struct {
Message string `json:"message"`
Type string `json:"type"` // "info" | "error" | "success"
Title string `json:"title,omitempty"`
Message string `json:"message"`
Variant string `json:"variant"` // "info" | "error" | "success" | "warning"
Duration int `json:"duration,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Invalid JSON body")
return
}

// Publish event for TUI clients via SSE
event.PublishSync(event.Event{
Type: event.TuiToastShow,
Data: event.TuiToastShowData{
Title: req.Title,
Message: req.Message,
Variant: req.Variant,
Duration: req.Duration,
},
})

writeSuccess(w)
}

// tuiPublish handles POST /tui/publish
// tuiPublish handles POST /tui/publish (generic event publisher)
func (s *Server) tuiPublish(w http.ResponseWriter, r *http.Request) {
var req struct {
Event string `json:"event"`
Data any `json:"data"`
Type string `json:"type"`
Properties json.RawMessage `json:"properties"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "Invalid JSON body")
return
}

switch req.Type {
case "tui.prompt.append":
var data event.TuiPromptAppendData
if err := json.Unmarshal(req.Properties, &data); err != nil {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "invalid properties")
return
}
event.PublishSync(event.Event{Type: event.TuiPromptAppend, Data: data})

case "tui.command.execute":
var data event.TuiCommandExecuteData
if err := json.Unmarshal(req.Properties, &data); err != nil {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "invalid properties")
return
}
event.PublishSync(event.Event{Type: event.TuiCommandExecute, Data: data})

case "tui.toast.show":
var data event.TuiToastShowData
if err := json.Unmarshal(req.Properties, &data); err != nil {
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "invalid properties")
return
}
event.PublishSync(event.Event{Type: event.TuiToastShow, Data: data})

default:
writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, "unknown event type: "+req.Type)
return
}

writeSuccess(w)
}

Expand Down
Loading
Loading