Skip to content

Commit b00326a

Browse files
committed
wip: refactoring tui
1 parent 4cf0aeb commit b00326a

14 files changed

Lines changed: 229 additions & 1602 deletions

File tree

packages/opencode/script/release.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ for (const [os, arch] of targets) {
2828
console.log(`building ${os}-${arch}`)
2929
const name = `${pkg.name}-${os}-${arch}`
3030
await $`mkdir -p dist/${name}/bin`
31-
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X github.com/sst/opencode/internal/version.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/main.go`.cwd(
31+
await $`GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X github.com/sst/opencode/internal/version.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
3232
"../tui",
3333
)
3434
await $`bun build --define OPENCODE_VERSION="'${version}'" --compile --minify --target=bun-${os}-${arch} --outfile=dist/${name}/bin/opencode ./src/index.ts ./dist/${name}/bin/tui`

packages/opencode/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ cli.command("", "Start the opencode in interactive mode").action(async () => {
2424
const server = Server.listen()
2525

2626
let cmd = ["go", "run", "./main.go"]
27-
let cwd = new URL("../../tui", import.meta.url).pathname
27+
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
2828
if (Bun.embeddedFiles.length > 0) {
2929
const blob = Bun.embeddedFiles[0] as File
3030
const binary = path.join(Global.cache(), "tui", blob.name)

packages/opencode/src/tool/edit.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from "path"
33
import { Tool } from "./tool"
44
import { FileTimes } from "./util/file-times"
55
import { LSP } from "../lsp"
6-
import { diffLines } from "diff"
6+
import { createTwoFilesPatch, diffLines } from "diff"
77
import { Permission } from "../permission"
88

99
const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files.
@@ -128,6 +128,7 @@ export const EditTool = Tool.define({
128128
})()
129129

130130
const changes = diffLines(contentOld, contentNew)
131+
const diff = createTwoFilesPatch(filePath, filePath, contentOld, contentNew)
131132

132133
FileTimes.read(ctx.sessionID, filePath)
133134

@@ -147,6 +148,7 @@ export const EditTool = Tool.define({
147148
metadata: {
148149
diagnostics,
149150
changes,
151+
diff,
150152
},
151153
output,
152154
}

packages/tui/cmd/opencode/main.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"os"
7+
"path/filepath"
8+
"sync"
9+
"time"
10+
11+
tea "github.com/charmbracelet/bubbletea"
12+
zone "github.com/lrstanley/bubblezone"
13+
"github.com/sst/opencode/internal/pubsub"
14+
"github.com/sst/opencode/internal/tui"
15+
"github.com/sst/opencode/internal/tui/app"
16+
"github.com/sst/opencode/pkg/client"
17+
)
18+
19+
func main() {
20+
url := "http://localhost:16713"
21+
httpClient, err := client.NewClientWithResponses(url)
22+
if err != nil {
23+
slog.Error("Failed to create client", "error", err)
24+
os.Exit(1)
25+
}
26+
paths, _ := httpClient.PostPathGetWithResponse(context.Background())
27+
logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
28+
29+
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
30+
err := os.MkdirAll(filepath.Dir(logfile), 0755)
31+
if err != nil {
32+
slog.Error("Failed to create log directory", "error", err)
33+
os.Exit(1)
34+
}
35+
}
36+
file, err := os.Create(logfile)
37+
if err != nil {
38+
slog.Error("Failed to create log file", "error", err)
39+
os.Exit(1)
40+
}
41+
defer file.Close()
42+
logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
43+
slog.SetDefault(logger)
44+
45+
// Create main context for the application
46+
ctx, cancel := context.WithCancel(context.Background())
47+
defer cancel()
48+
49+
app, err := app.New(ctx, httpClient)
50+
if err != nil {
51+
slog.Error("Failed to create app", "error", err)
52+
// return err
53+
}
54+
55+
// Set up the TUI
56+
zone.NewGlobal()
57+
program := tea.NewProgram(
58+
tui.New(app),
59+
tea.WithAltScreen(),
60+
)
61+
62+
eventClient, err := client.NewClient(url)
63+
if err != nil {
64+
slog.Error("Failed to create event client", "error", err)
65+
os.Exit(1)
66+
}
67+
68+
evts, err := eventClient.Event(ctx)
69+
if err != nil {
70+
slog.Error("Failed to subscribe to events", "error", err)
71+
os.Exit(1)
72+
}
73+
74+
go func() {
75+
for item := range evts {
76+
program.Send(item)
77+
}
78+
}()
79+
80+
// Setup the subscriptions, this will send services events to the TUI
81+
ch, cancelSubs := setupSubscriptions(app, ctx)
82+
83+
// Create a context for the TUI message handler
84+
tuiCtx, tuiCancel := context.WithCancel(ctx)
85+
var tuiWg sync.WaitGroup
86+
tuiWg.Add(1)
87+
88+
// Set up message handling for the TUI
89+
go func() {
90+
defer tuiWg.Done()
91+
// defer logging.RecoverPanic("TUI-message-handler", func() {
92+
// attemptTUIRecovery(program)
93+
// })
94+
95+
for {
96+
select {
97+
case <-tuiCtx.Done():
98+
slog.Info("TUI message handler shutting down")
99+
return
100+
case msg, ok := <-ch:
101+
if !ok {
102+
slog.Info("TUI message channel closed")
103+
return
104+
}
105+
program.Send(msg)
106+
}
107+
}
108+
}()
109+
110+
// Cleanup function for when the program exits
111+
cleanup := func() {
112+
// Cancel subscriptions first
113+
cancelSubs()
114+
115+
// Then shutdown the app
116+
app.Shutdown()
117+
118+
// Then cancel TUI message handler
119+
tuiCancel()
120+
121+
// Wait for TUI message handler to finish
122+
tuiWg.Wait()
123+
124+
slog.Info("All goroutines cleaned up")
125+
}
126+
127+
// Run the TUI
128+
result, err := program.Run()
129+
cleanup()
130+
131+
if err != nil {
132+
slog.Error("TUI error", "error", err)
133+
// return fmt.Errorf("TUI error: %v", err)
134+
}
135+
136+
slog.Info("TUI exited", "result", result)
137+
}
138+
139+
func setupSubscriber[T any](
140+
ctx context.Context,
141+
wg *sync.WaitGroup,
142+
name string,
143+
subscriber func(context.Context) <-chan pubsub.Event[T],
144+
outputCh chan<- tea.Msg,
145+
) {
146+
wg.Add(1)
147+
go func() {
148+
defer wg.Done()
149+
// defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
150+
151+
subCh := subscriber(ctx)
152+
if subCh == nil {
153+
slog.Warn("subscription channel is nil", "name", name)
154+
return
155+
}
156+
157+
for {
158+
select {
159+
case event, ok := <-subCh:
160+
if !ok {
161+
slog.Info("subscription channel closed", "name", name)
162+
return
163+
}
164+
165+
var msg tea.Msg = event
166+
167+
select {
168+
case outputCh <- msg:
169+
case <-time.After(2 * time.Second):
170+
slog.Warn("message dropped due to slow consumer", "name", name)
171+
case <-ctx.Done():
172+
slog.Info("subscription cancelled", "name", name)
173+
return
174+
}
175+
case <-ctx.Done():
176+
slog.Info("subscription cancelled", "name", name)
177+
return
178+
}
179+
}
180+
}()
181+
}
182+
183+
func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
184+
ch := make(chan tea.Msg, 100)
185+
186+
wg := sync.WaitGroup{}
187+
ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
188+
189+
setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
190+
191+
cleanupFunc := func() {
192+
slog.Info("Cancelling all subscriptions")
193+
cancel() // Signal all goroutines to stop
194+
195+
waitCh := make(chan struct{})
196+
go func() {
197+
// defer logging.RecoverPanic("subscription-cleanup", nil)
198+
wg.Wait()
199+
close(waitCh)
200+
}()
201+
202+
select {
203+
case <-waitCh:
204+
slog.Info("All subscription goroutines completed successfully")
205+
close(ch) // Only close after all writers are confirmed done
206+
case <-time.After(5 * time.Second):
207+
slog.Warn("Timed out waiting for some subscription goroutines to complete")
208+
close(ch)
209+
}
210+
}
211+
return ch, cleanupFunc
212+
}

0 commit comments

Comments
 (0)