Skip to content

Commit f20ef61

Browse files
committed
wip: api for tui
1 parent 5611ef8 commit f20ef61

18 files changed

Lines changed: 594 additions & 39 deletions

File tree

packages/opencode/src/cli/cmd/serve.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Provider } from "../../provider/provider"
22
import { Server } from "../../server/server"
3-
import { Share } from "../../share/share"
43
import { bootstrap } from "../bootstrap"
54
import { cmd } from "./cmd"
65

@@ -32,7 +31,6 @@ export const ServeCommand = cmd({
3231
const hostname = args.hostname
3332
const port = args.port
3433

35-
await Share.init()
3634
const server = Server.listen({
3735
port,
3836
hostname,

packages/opencode/src/cli/cmd/tui.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ export const TuiCommand = cmd({
3636
.option("mode", {
3737
type: "string",
3838
describe: "mode to use",
39+
})
40+
.option("port", {
41+
type: "number",
42+
describe: "port to listen on",
43+
default: 0,
44+
})
45+
.option("hostname", {
46+
alias: ["h"],
47+
type: "string",
48+
describe: "hostname to listen on",
49+
default: "127.0.0.1",
3950
}),
4051
handler: async (args) => {
4152
while (true) {
@@ -54,8 +65,8 @@ export const TuiCommand = cmd({
5465
}
5566

5667
const server = Server.listen({
57-
port: 0,
58-
hostname: "127.0.0.1",
68+
port: args.port,
69+
hostname: args.hostname,
5970
})
6071

6172
let cmd = ["go", "run", "./main.go"]

packages/opencode/src/server/server.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { File } from "../file"
1717
import { LSP } from "../lsp"
1818
import { MessageV2 } from "../session/message-v2"
1919
import { Mode } from "../session/mode"
20+
import { callTui, TuiRoute } from "./tui"
2021

2122
const ERRORS = {
2223
400: {
@@ -703,6 +704,48 @@ export namespace Server {
703704
return c.json(modes)
704705
},
705706
)
707+
.post(
708+
"/tui/prompt",
709+
describeRoute({
710+
description: "Send a prompt to the TUI",
711+
responses: {
712+
200: {
713+
description: "Prompt processed successfully",
714+
content: {
715+
"application/json": {
716+
schema: resolver(z.boolean()),
717+
},
718+
},
719+
},
720+
},
721+
}),
722+
zValidator(
723+
"json",
724+
z.object({
725+
text: z.string(),
726+
parts: MessageV2.Part.array(),
727+
}),
728+
),
729+
async (c) => c.json(await callTui(c)),
730+
)
731+
.post(
732+
"/tui/open-help",
733+
describeRoute({
734+
description: "Open the help dialog",
735+
responses: {
736+
200: {
737+
description: "Help dialog opened successfully",
738+
content: {
739+
"application/json": {
740+
schema: resolver(z.boolean()),
741+
},
742+
},
743+
},
744+
},
745+
}),
746+
async (c) => c.json(await callTui(c)),
747+
)
748+
.route("/tui/control", TuiRoute)
706749

707750
return result
708751
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Hono, type Context } from "hono"
2+
import { AsyncQueue } from "../util/queue"
3+
4+
interface Request {
5+
path: string
6+
body: any
7+
}
8+
9+
const request = new AsyncQueue<Request>()
10+
const response = new AsyncQueue<any>()
11+
12+
export async function callTui(ctx: Context) {
13+
const body = await ctx.req.json()
14+
request.push({
15+
path: ctx.req.path,
16+
body,
17+
})
18+
return response.next()
19+
}
20+
21+
export const TuiRoute = new Hono()
22+
.get("/next", async (c) => {
23+
const req = await request.next()
24+
return c.json(req)
25+
})
26+
.post("/response", async (c) => {
27+
const body = await c.req.json()
28+
response.push(body)
29+
return c.json(true)
30+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class AsyncQueue<T> implements AsyncIterable<T> {
2+
private queue: T[] = []
3+
private resolvers: ((value: T) => void)[] = []
4+
5+
push(item: T) {
6+
const resolve = this.resolvers.shift()
7+
if (resolve) resolve(item)
8+
else this.queue.push(item)
9+
}
10+
11+
async next(): Promise<T> {
12+
if (this.queue.length > 0) return this.queue.shift()!
13+
return new Promise((resolve) => this.resolvers.push(resolve))
14+
}
15+
16+
async *[Symbol.asyncIterator]() {
17+
while (true) yield await this.next()
18+
}
19+
}

packages/tui/cmd/opencode/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
flag "github.com/spf13/pflag"
1414
"github.com/sst/opencode-sdk-go"
1515
"github.com/sst/opencode-sdk-go/option"
16+
"github.com/sst/opencode/internal/api"
1617
"github.com/sst/opencode/internal/app"
1718
"github.com/sst/opencode/internal/clipboard"
1819
"github.com/sst/opencode/internal/tui"
@@ -100,6 +101,8 @@ func main() {
100101
}
101102
}()
102103

104+
go api.Start(ctx, program, httpClient)
105+
103106
// Handle signals in a separate goroutine
104107
go func() {
105108
sig := <-sigChan

packages/tui/internal/api/api.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"log"
7+
8+
tea "github.com/charmbracelet/bubbletea/v2"
9+
"github.com/sst/opencode-sdk-go"
10+
)
11+
12+
type Request struct {
13+
Path string `json:"path"`
14+
Body json.RawMessage `json:"body"`
15+
}
16+
17+
func Start(ctx context.Context, program *tea.Program, client *opencode.Client) {
18+
for {
19+
select {
20+
case <-ctx.Done():
21+
return
22+
default:
23+
var req Request
24+
if err := client.Get(ctx, "/tui/control/next", nil, &req); err != nil {
25+
log.Printf("Error getting next request: %v", err)
26+
continue
27+
}
28+
program.Send(req)
29+
}
30+
}
31+
}
32+
33+
func Reply(ctx context.Context, client *opencode.Client, response interface{}) tea.Cmd {
34+
return func() tea.Msg {
35+
err := client.Post(ctx, "/tui/control/response", response, nil)
36+
if err != nil {
37+
return err
38+
}
39+
return nil
40+
}
41+
}

packages/tui/internal/tui/tui.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tui
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"log/slog"
78
"os"
@@ -15,6 +16,7 @@ import (
1516
"github.com/charmbracelet/lipgloss/v2"
1617

1718
"github.com/sst/opencode-sdk-go"
19+
"github.com/sst/opencode/internal/api"
1820
"github.com/sst/opencode/internal/app"
1921
"github.com/sst/opencode/internal/commands"
2022
"github.com/sst/opencode/internal/completions"
@@ -57,7 +59,7 @@ const (
5759
const interruptDebounceTimeout = 1 * time.Second
5860
const exitDebounceTimeout = 1 * time.Second
5961

60-
type appModel struct {
62+
type Model struct {
6163
width, height int
6264
app *app.App
6365
modal layout.Modal
@@ -78,7 +80,7 @@ type appModel struct {
7880
fileViewer fileviewer.Model
7981
}
8082

81-
func (a appModel) Init() tea.Cmd {
83+
func (a Model) Init() tea.Cmd {
8284
var cmds []tea.Cmd
8385
// https://github.com/charmbracelet/bubbletea/issues/1440
8486
// https://github.com/sst/opencode/issues/127
@@ -102,7 +104,7 @@ func (a appModel) Init() tea.Cmd {
102104
return tea.Batch(cmds...)
103105
}
104106

105-
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107+
func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
106108
measure := util.Measure("app.Update")
107109
defer measure("from", fmt.Sprintf("%T", msg))
108110

@@ -499,6 +501,26 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
499501
a.editor.SetExitKeyInDebounce(false)
500502
case dialog.FindSelectedMsg:
501503
return a.openFile(msg.FilePath)
504+
505+
// API
506+
case api.Request:
507+
slog.Info("api", "path", msg.Path)
508+
var response any = true
509+
switch msg.Path {
510+
case "/tui/open-help":
511+
helpDialog := dialog.NewHelpDialog(a.app)
512+
a.modal = helpDialog
513+
case "/tui/prompt":
514+
var body struct {
515+
Text string `json:"text"`
516+
Parts []opencode.Part `json:"parts"`
517+
}
518+
json.Unmarshal((msg.Body), &body)
519+
a.editor.SetValue(body.Text)
520+
default:
521+
break
522+
}
523+
cmds = append(cmds, api.Reply(context.Background(), a.app.Client, response))
502524
}
503525

504526
s, cmd := a.status.Update(msg)
@@ -532,7 +554,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
532554
return a, tea.Batch(cmds...)
533555
}
534556

535-
func (a appModel) View() string {
557+
func (a Model) View() string {
536558
measure := util.Measure("app.View")
537559
defer measure()
538560
t := theme.CurrentTheme()
@@ -569,7 +591,7 @@ func (a appModel) View() string {
569591
return mainLayout + "\n" + a.status.View()
570592
}
571593

572-
func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
594+
func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
573595
var cmd tea.Cmd
574596
response, err := a.app.Client.File.Read(
575597
context.Background(),
@@ -589,7 +611,7 @@ func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
589611
return a, cmd
590612
}
591613

592-
func (a appModel) home() string {
614+
func (a Model) home() string {
593615
measure := util.Measure("home.View")
594616
defer measure()
595617
t := theme.CurrentTheme()
@@ -726,7 +748,7 @@ func (a appModel) home() string {
726748
return mainLayout
727749
}
728750

729-
func (a appModel) chat() string {
751+
func (a Model) chat() string {
730752
measure := util.Measure("chat.View")
731753
defer measure()
732754
effectiveWidth := a.width - 4
@@ -774,7 +796,7 @@ func (a appModel) chat() string {
774796
return mainLayout
775797
}
776798

777-
func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
799+
func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
778800
var cmd tea.Cmd
779801
cmds := []tea.Cmd{
780802
util.CmdHandler(commands.CommandExecutedMsg(command)),
@@ -1057,7 +1079,7 @@ func NewModel(app *app.App) tea.Model {
10571079
leaderBinding = &binding
10581080
}
10591081

1060-
model := &appModel{
1082+
model := &Model{
10611083
status: status.NewStatusCmp(app),
10621084
app: app,
10631085
editor: editor,

packages/tui/sdk/.stats.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
configured_endpoints: 22
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e7f4ac9b5afd5c6db4741a27b5445167808b0a3b7c36dfd525bfb3446a11a253.yml
3-
openapi_spec_hash: 3e7b367a173d6de7924f35a41ac6b5a5
4-
config_hash: 6d56a7ca0d6ed899ecdb5c053a8278ae
1+
configured_endpoints: 24
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d10809ab68e48a338167e5504d69db2a0a80739adf6ecd3f065644a4139bc374.yml
3+
openapi_spec_hash: 4875565ef8df3446dbab11f450e04c51
4+
config_hash: 0032a76356d31c6b4c218b39fff635bb

packages/tui/sdk/api.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ Methods:
1919
Response Types:
2020

2121
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#App">App</a>
22-
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#LogLevel">LogLevel</a>
2322
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Mode">Mode</a>
2423
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
2524
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
@@ -76,12 +75,23 @@ Methods:
7675

7776
Params Types:
7877

78+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartParam">FilePartParam</a>
7979
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartInputParam">FilePartInputParam</a>
8080
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceUnionParam">FilePartSourceUnionParam</a>
8181
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FilePartSourceTextParam">FilePartSourceTextParam</a>
8282
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileSourceParam">FileSourceParam</a>
83+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#PartUnionParam">PartUnionParam</a>
84+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SnapshotPartParam">SnapshotPartParam</a>
85+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepFinishPartParam">StepFinishPartParam</a>
86+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#StepStartPartParam">StepStartPartParam</a>
8387
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SymbolSourceParam">SymbolSourceParam</a>
88+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartParam">TextPartParam</a>
8489
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TextPartInputParam">TextPartInputParam</a>
90+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolPartParam">ToolPartParam</a>
91+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateCompletedParam">ToolStateCompletedParam</a>
92+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateErrorParam">ToolStateErrorParam</a>
93+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStatePendingParam">ToolStatePendingParam</a>
94+
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunningParam">ToolStateRunningParam</a>
8595

8696
Response Types:
8797

@@ -118,3 +128,10 @@ Methods:
118128
- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
119129
- <code title="post /session/{id}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
120130
- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
131+
132+
# Tui
133+
134+
Methods:
135+
136+
- <code title="post /tui/open-help">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenHelp">OpenHelp</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
137+
- <code title="post /tui/prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.Prompt">Prompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiPromptParams">TuiPromptParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

0 commit comments

Comments
 (0)