Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit e7bb99b

Browse files
committed
fix the memory bug
1 parent 1da298e commit e7bb99b

File tree

6 files changed

+127
-55
lines changed

6 files changed

+127
-55
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,12 @@ go build -o opencode
351351

352352
## Acknowledgments
353353

354-
OpenCode builds upon the work of several open source projects and developers:
354+
OpenCode gratefully acknowledges the contributions and support from these key individuals:
355355

356-
- [@isaacphi](https://github.com/isaacphi) - LSP client implementation
356+
- [@isaacphi](https://github.com/isaacphi) - For the [mcp-language-server](https://github.com/isaacphi/mcp-language-server) project which provided the foundation for our LSP client implementation
357+
- [@adamdottv](https://github.com/adamdottv) - For the design direction and UI/UX architecture
358+
359+
Special thanks to the broader open source community whose tools and libraries have made this project possible.
357360

358361
## License
359362

cmd/root.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ var rootCmd = &cobra.Command{
7979
initMCPTools(ctx, app)
8080

8181
// Setup the subscriptions, this will send services events to the TUI
82-
ch, cancelSubs := setupSubscriptions(app)
82+
ch, cancelSubs := setupSubscriptions(app, ctx)
8383

8484
// Create a context for the TUI message handler
8585
tuiCtx, tuiCancel := context.WithCancel(ctx)
@@ -174,21 +174,21 @@ func setupSubscriber[T any](
174174
defer wg.Done()
175175
defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
176176

177+
subCh := subscriber(ctx)
178+
177179
for {
178180
select {
179-
case event, ok := <-subscriber(ctx):
181+
case event, ok := <-subCh:
180182
if !ok {
181183
logging.Info("%s subscription channel closed", name)
182184
return
183185
}
184186

185-
// Convert generic event to tea.Msg if needed
186187
var msg tea.Msg = event
187188

188-
// Non-blocking send with timeout to prevent deadlocks
189189
select {
190190
case outputCh <- msg:
191-
case <-time.After(500 * time.Millisecond):
191+
case <-time.After(2 * time.Second):
192192
logging.Warn("%s message dropped due to slow consumer", name)
193193
case <-ctx.Done():
194194
logging.Info("%s subscription cancelled", name)
@@ -202,23 +202,21 @@ func setupSubscriber[T any](
202202
}()
203203
}
204204

205-
func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
205+
func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
206206
ch := make(chan tea.Msg, 100)
207-
// Add a buffer to prevent blocking
207+
208208
wg := sync.WaitGroup{}
209-
ctx, cancel := context.WithCancel(context.Background())
210-
// Setup each subscription using the helper
209+
ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
210+
211211
setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
212212
setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
213213
setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
214214
setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
215215

216-
// Return channel and a cleanup function
217216
cleanupFunc := func() {
218217
logging.Info("Cancelling all subscriptions")
219218
cancel() // Signal all goroutines to stop
220219

221-
// Wait with a timeout for all goroutines to complete
222220
waitCh := make(chan struct{})
223221
go func() {
224222
defer logging.RecoverPanic("subscription-cleanup", nil)
@@ -229,11 +227,11 @@ func setupSubscriptions(app *app.App) (chan tea.Msg, func()) {
229227
select {
230228
case <-waitCh:
231229
logging.Info("All subscription goroutines completed successfully")
230+
close(ch) // Only close after all writers are confirmed done
232231
case <-time.After(5 * time.Second):
233232
logging.Warn("Timed out waiting for some subscription goroutines to complete")
233+
close(ch)
234234
}
235-
236-
close(ch) // Safe to close after all writers are done or timed out
237235
}
238236
return ch, cleanupFunc
239237
}

internal/pubsub/broker.go

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,53 @@ import (
55
"sync"
66
)
77

8-
const bufferSize = 1024
8+
const bufferSize = 64
99

10-
// Broker allows clients to publish events and subscribe to events
1110
type Broker[T any] struct {
12-
subs map[chan Event[T]]struct{} // subscriptions
13-
mu sync.Mutex // sync access to map
14-
done chan struct{} // close when broker is shutting down
11+
subs map[chan Event[T]]struct{}
12+
mu sync.RWMutex
13+
done chan struct{}
14+
subCount int
15+
maxEvents int
1516
}
1617

17-
// NewBroker constructs a pub/sub broker.
1818
func NewBroker[T any]() *Broker[T] {
19+
return NewBrokerWithOptions[T](bufferSize, 1000)
20+
}
21+
22+
func NewBrokerWithOptions[T any](channelBufferSize, maxEvents int) *Broker[T] {
1923
b := &Broker[T]{
20-
subs: make(map[chan Event[T]]struct{}),
21-
done: make(chan struct{}),
24+
subs: make(map[chan Event[T]]struct{}),
25+
done: make(chan struct{}),
26+
subCount: 0,
27+
maxEvents: maxEvents,
2228
}
2329
return b
2430
}
2531

26-
// Shutdown the broker, terminating any subscriptions.
2732
func (b *Broker[T]) Shutdown() {
28-
close(b.done)
33+
select {
34+
case <-b.done: // Already closed
35+
return
36+
default:
37+
close(b.done)
38+
}
2939

3040
b.mu.Lock()
3141
defer b.mu.Unlock()
3242

33-
// Remove each subscriber entry, so Publish() cannot send any further
34-
// messages, and close each subscriber's channel, so the subscriber cannot
35-
// consume any more messages.
3643
for ch := range b.subs {
3744
delete(b.subs, ch)
3845
close(ch)
3946
}
47+
48+
b.subCount = 0
4049
}
4150

42-
// Subscribe subscribes the caller to a stream of events. The returned channel
43-
// is closed when the broker is shutdown.
4451
func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
4552
b.mu.Lock()
4653
defer b.mu.Unlock()
4754

48-
// Check if broker has shutdown and if so return closed channel
4955
select {
5056
case <-b.done:
5157
ch := make(chan Event[T])
@@ -54,18 +60,16 @@ func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
5460
default:
5561
}
5662

57-
// Subscribe
5863
sub := make(chan Event[T], bufferSize)
5964
b.subs[sub] = struct{}{}
65+
b.subCount++
6066

61-
// Unsubscribe when context is done.
6267
go func() {
6368
<-ctx.Done()
6469

6570
b.mu.Lock()
6671
defer b.mu.Unlock()
6772

68-
// Check if broker has shutdown and if so do nothing
6973
select {
7074
case <-b.done:
7175
return
@@ -74,21 +78,39 @@ func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] {
7478

7579
delete(b.subs, sub)
7680
close(sub)
81+
b.subCount--
7782
}()
7883

7984
return sub
8085
}
8186

82-
// Publish an event to subscribers.
87+
func (b *Broker[T]) GetSubscriberCount() int {
88+
b.mu.RLock()
89+
defer b.mu.RUnlock()
90+
return b.subCount
91+
}
92+
8393
func (b *Broker[T]) Publish(t EventType, payload T) {
84-
b.mu.Lock()
85-
defer b.mu.Unlock()
94+
b.mu.RLock()
95+
select {
96+
case <-b.done:
97+
b.mu.RUnlock()
98+
return
99+
default:
100+
}
86101

102+
subscribers := make([]chan Event[T], 0, len(b.subs))
87103
for sub := range b.subs {
104+
subscribers = append(subscribers, sub)
105+
}
106+
b.mu.RUnlock()
107+
108+
event := Event[T]{Type: t, Payload: payload}
109+
110+
for _, sub := range subscribers {
88111
select {
89-
case sub <- Event[T]{Type: t, Payload: payload}:
90-
case <-b.done:
91-
return
112+
case sub <- event:
113+
default:
92114
}
93115
}
94116
}

internal/tui/components/chat/list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
370370
delete(m.cachedContent, msg.ID)
371371
}
372372
m.uiMessages = make([]uiMessage, 0)
373+
m.renderView()
373374
return nil
374375
}
375376

internal/tui/components/core/status.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import (
1818
"github.com/kujtimiihoxha/opencode/internal/tui/util"
1919
)
2020

21+
type StatusCmp interface {
22+
tea.Model
23+
SetHelpMsg(string)
24+
}
25+
2126
type statusCmp struct {
2227
info util.InfoMsg
2328
width int
@@ -146,15 +151,15 @@ func (m *statusCmp) projectDiagnostics() string {
146151
break
147152
}
148153
}
149-
154+
150155
// If any server is initializing, show that status
151156
if initializing {
152157
return lipgloss.NewStyle().
153158
Background(styles.BackgroundDarker).
154159
Foreground(styles.Peach).
155160
Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
156161
}
157-
162+
158163
errorDiagnostics := []protocol.Diagnostic{}
159164
warnDiagnostics := []protocol.Diagnostic{}
160165
hintDiagnostics := []protocol.Diagnostic{}
@@ -235,7 +240,11 @@ func (m statusCmp) model() string {
235240
return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
236241
}
237242

238-
func NewStatusCmp(lspClients map[string]*lsp.Client) tea.Model {
243+
func (m statusCmp) SetHelpMsg(s string) {
244+
helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
245+
}
246+
247+
func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
239248
return &statusCmp{
240249
messageTTL: 10 * time.Second,
241250
lspClients: lspClients,

0 commit comments

Comments
 (0)